fix: enhance MCP token handling (#14483)

fix #14475 

#### PR Dependency Tree


* **PR #14483** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Enhanced MCP server token management with improved security—tokens now
display only once with redaction support.
* Updated token creation and deletion workflows with clearer UI state
controls.
* Added tooltip guidance when copying configuration with redacted
tokens.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-21 04:14:14 +08:00
committed by GitHub
parent c9bffc13b5
commit da57bfe8e7
4 changed files with 33 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
import { Button, ErrorMessage, notify, Skeleton } from '@affine/component'; import { Button, ErrorMessage, notify, Skeleton } from '@affine/component';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { AccessTokenService, ServerService } from '@affine/core/modules/cloud'; import { AccessTokenService, ServerService } from '@affine/core/modules/cloud';
import type { AccessToken } from '@affine/core/modules/cloud/stores/access-token';
import { WorkspaceService } from '@affine/core/modules/workspace'; import { WorkspaceService } from '@affine/core/modules/workspace';
import { UserFriendlyError } from '@affine/error'; import { UserFriendlyError } from '@affine/error';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -37,23 +38,30 @@ const McpServerSetting = () => {
const isRevalidating = useLiveData(accessTokenService.isRevalidating$); const isRevalidating = useLiveData(accessTokenService.isRevalidating$);
const error = useLiveData(accessTokenService.error$); const error = useLiveData(accessTokenService.error$);
const [mutating, setMutating] = useState(false); const [mutating, setMutating] = useState(false);
const [revealedAccessToken, setRevealedAccessToken] =
useState<AccessToken | null>(null);
const t = useI18n(); const t = useI18n();
const mcpAccessToken = useMemo(() => { const mcpAccessToken = useMemo(() => {
return accessTokens?.find(token => token.name === 'mcp'); return accessTokens?.find(token => token.name === 'mcp');
}, [accessTokens]); }, [accessTokens]);
const displayedToken = revealedAccessToken ?? mcpAccessToken;
const hasMcpToken = Boolean(revealedAccessToken || mcpAccessToken);
const hasCopyableToken = Boolean(revealedAccessToken);
const isRedactedDisplay = hasMcpToken && !hasCopyableToken;
const code = useMemo(() => { const code = useMemo(() => {
return mcpAccessToken return displayedToken
? JSON.stringify( ? JSON.stringify(
{ {
mcpServers: { mcpServers: {
[`${workspaceName} - AFFiNE`]: { [`affine_workspace_${workspaceService.workspace.id}`]: {
type: 'streamable-http', type: 'streamable-http',
url: `${serverService.server.baseUrl}/api/workspaces/${workspaceService.workspace.id}/mcp`, url: `${serverService.server.baseUrl}/api/workspaces/${workspaceService.workspace.id}/mcp`,
note: 'Read docs from AFFiNE workspace', note: `Read docs from AFFiNE workspace "${workspaceName}"`,
headers: { headers: {
Authorization: `Bearer ${mcpAccessToken.token}`, Authorization: `Bearer ${displayedToken.token}`,
}, },
}, },
}, },
@@ -62,7 +70,12 @@ const McpServerSetting = () => {
2 2
) )
: null; : null;
}, [mcpAccessToken, workspaceName, workspaceService, serverService]); }, [displayedToken, workspaceName, workspaceService, serverService]);
const copyJsonDisabled = !code || mutating || isRedactedDisplay;
const copyJsonTooltip = isRedactedDisplay
? t['com.affine.integration.mcp-server.copy-json.disabled-hint']()
: undefined;
const showLoading = accessTokens === null && isRevalidating; const showLoading = accessTokens === null && isRevalidating;
const showError = accessTokens === null && error !== null; const showError = accessTokens === null && error !== null;
@@ -77,7 +90,9 @@ const McpServerSetting = () => {
if (mcpAccessToken) { if (mcpAccessToken) {
await accessTokenService.revokeUserAccessToken(mcpAccessToken.id); await accessTokenService.revokeUserAccessToken(mcpAccessToken.id);
} }
await accessTokenService.generateUserAccessToken('mcp'); const createdToken =
await accessTokenService.generateUserAccessToken('mcp');
setRevealedAccessToken(createdToken);
} catch (err) { } catch (err) {
notify.error({ notify.error({
error: UserFriendlyError.fromAny(err), error: UserFriendlyError.fromAny(err),
@@ -93,6 +108,7 @@ const McpServerSetting = () => {
if (mcpAccessToken) { if (mcpAccessToken) {
await accessTokenService.revokeUserAccessToken(mcpAccessToken.id); await accessTokenService.revokeUserAccessToken(mcpAccessToken.id);
} }
setRevealedAccessToken(null);
} catch (err) { } catch (err) {
notify.error({ notify.error({
error: UserFriendlyError.fromAny(err), error: UserFriendlyError.fromAny(err),
@@ -127,7 +143,7 @@ const McpServerSetting = () => {
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<div className={styles.sectionTitle}>Personal access token</div> <div className={styles.sectionTitle}>Personal access token</div>
{!mcpAccessToken ? ( {!hasMcpToken ? (
<Button <Button
variant="primary" variant="primary"
onClick={handleGenerateAccessToken} onClick={handleGenerateAccessToken}
@@ -164,7 +180,8 @@ const McpServerSetting = () => {
title: t['Copied to clipboard'](), title: t['Copied to clipboard'](),
}); });
}} }}
disabled={!code || mutating} disabled={copyJsonDisabled}
tooltip={copyJsonTooltip}
> >
Copy json Copy json
</Button> </Button>

View File

@@ -24,7 +24,7 @@ export class AccessTokenService extends Service {
isRevalidating$ = new LiveData(false); isRevalidating$ = new LiveData(false);
error$ = new LiveData<any>(null); error$ = new LiveData<any>(null);
async generateUserAccessToken(name: string) { async generateUserAccessToken(name: string): Promise<AccessToken> {
const accessToken = const accessToken =
await this.accessTokenStore.generateUserAccessToken(name); await this.accessTokenStore.generateUserAccessToken(name);
this.accessTokens$.value = [ this.accessTokens$.value = [
@@ -33,6 +33,8 @@ export class AccessTokenService extends Service {
]; ];
await this.waitForRevalidation(); await this.waitForRevalidation();
return accessToken as AccessToken;
} }
async revokeUserAccessToken(id: string) { async revokeUserAccessToken(id: string) {

View File

@@ -8497,6 +8497,10 @@ export function useAFFiNEI18N(): {
* `Enable other MCP Client to search and read the doc of AFFiNE.` * `Enable other MCP Client to search and read the doc of AFFiNE.`
*/ */
["com.affine.integration.mcp-server.desc"](): string; ["com.affine.integration.mcp-server.desc"](): string;
/**
* `The MCP token is shown only once. Delete and recreate it to copy the JSON configuration.`
*/
["com.affine.integration.mcp-server.copy-json.disabled-hint"](): string;
/** /**
* `Notes` * `Notes`
*/ */

View File

@@ -2129,6 +2129,7 @@
"com.affine.integration.calendar.no-calendar": "No subscribed calendars yet.", "com.affine.integration.calendar.no-calendar": "No subscribed calendars yet.",
"com.affine.integration.mcp-server.name": "MCP Server", "com.affine.integration.mcp-server.name": "MCP Server",
"com.affine.integration.mcp-server.desc": "Enable other MCP Client to search and read the doc of AFFiNE.", "com.affine.integration.mcp-server.desc": "Enable other MCP Client to search and read the doc of AFFiNE.",
"com.affine.integration.mcp-server.copy-json.disabled-hint": "The MCP token is shown only once. Delete and recreate it to copy the JSON configuration.",
"com.affine.audio.notes": "Notes", "com.affine.audio.notes": "Notes",
"com.affine.audio.transcribing": "Transcribing", "com.affine.audio.transcribing": "Transcribing",
"com.affine.audio.transcribe.non-owner.confirm.title": "Unable to retrieve AI results for others", "com.affine.audio.transcribe.non-owner.confirm.title": "Unable to retrieve AI results for others",