mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 18:02:47 +08:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user