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