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

View File

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

View File

@@ -2129,6 +2129,7 @@
"com.affine.integration.calendar.no-calendar": "No subscribed calendars yet.",
"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.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.transcribing": "Transcribing",
"com.affine.audio.transcribe.non-owner.confirm.title": "Unable to retrieve AI results for others",