Skip to content

Commit eddaf26

Browse files
committed
Add auth prepare support to notion-mcp, too.
1 parent 7c8b89f commit eddaf26

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

src/services/notion-mcp.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* This is separate from the existing Notion service which uses internal integration tokens.
66
*/
77

8+
import { z } from 'zod';
89
import type { Browser, BrowserContext, Response } from 'playwright';
910
import { type ApiCredentials, OAuthCredentials } from '../apiCredentials/base.js';
1011
import { runCaptured } from '../curl.js';
@@ -20,9 +21,24 @@ import {
2021
ServiceSession,
2122
LoginFailedError,
2223
LoginCancelledError,
24+
buildPreparedCredentials,
2325
isBrowserClosedError,
2426
} from './core/base.js';
2527

28+
/**
29+
* JSON accepted by `latchkey auth prepare notion-mcp`: the OAuth client id to
30+
* reuse instead of registering a new client dynamically at mcp.notion.com.
31+
* Notion MCP is a public client, so no secret is needed. `.strict()` rejects
32+
* unknown keys so typos are reported instead of silently ignored.
33+
*/
34+
export const NotionMcpPrepareInputSchema = z
35+
.object({
36+
clientId: z.string().min(1),
37+
})
38+
.strict();
39+
40+
export type NotionMcpPrepareInput = z.infer<typeof NotionMcpPrepareInputSchema>;
41+
2642
const TOKEN_ENDPOINT = 'https://mcp.notion.com/token';
2743
const REGISTRATION_ENDPOINT = 'https://mcp.notion.com/register';
2844
const AUTHORIZATION_ENDPOINT = 'https://mcp.notion.com/authorize';
@@ -204,6 +220,20 @@ export class NotionMcp extends Service {
204220
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
205221
}
206222

223+
/**
224+
* Notion MCP accepts an OAuth client id prepared in advance via
225+
* `latchkey auth prepare`, stored as token-less OAuth credentials until login.
226+
* The login flow reuses this client id instead of registering a new client.
227+
*/
228+
override prepareFromJson(parsedJson: unknown): ApiCredentials {
229+
return buildPreparedCredentials(
230+
this.name,
231+
NotionMcpPrepareInputSchema,
232+
parsedJson,
233+
({ clientId }) => new OAuthCredentials(clientId, '')
234+
);
235+
}
236+
207237
override getSession(appNamePrefix: string): NotionMcpSession {
208238
return new NotionMcpSession(this, appNamePrefix);
209239
}

tests/sharedOperations.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Service,
1414
} from '../src/services/core/base.js';
1515
import { GOOGLE_GMAIL } from '../src/services/google/gmail.js';
16+
import { NOTION_MCP } from '../src/services/notion-mcp.js';
1617
import { RegisteredService } from '../src/services/core/registered.js';
1718
import { ServiceRegistry } from '../src/serviceRegistry.js';
1819
import { Config } from '../src/config.js';
@@ -443,5 +444,51 @@ describe('operations', () => {
443444
)
444445
).toThrow(PrepareInputInvalidError);
445446
});
447+
448+
it('stores a token-less OAuth client id for notion-mcp (public client, no secret)', () => {
449+
const registry = new ServiceRegistry([NOTION_MCP]);
450+
const store = createApiCredentialStore();
451+
452+
const result = prepareService(
453+
registry,
454+
store,
455+
'notion-mcp',
456+
JSON.stringify({ clientId: 'notion-client-id' })
457+
);
458+
459+
expect(result).toEqual({ serviceName: 'notion-mcp', credentialType: 'oauth' });
460+
const stored = store.get('notion-mcp');
461+
expect(stored).toBeInstanceOf(OAuthCredentials);
462+
const oauth = stored as OAuthCredentials;
463+
expect(oauth.clientId).toBe('notion-client-id');
464+
expect(oauth.clientSecret).toBe('');
465+
expect(oauth.accessToken).toBeUndefined();
466+
expect(oauth.refreshToken).toBeUndefined();
467+
});
468+
469+
it('rejects a notion-mcp clientSecret (unknown key, strict schema)', () => {
470+
const registry = new ServiceRegistry([NOTION_MCP]);
471+
const store = createApiCredentialStore();
472+
473+
expect(() =>
474+
prepareService(
475+
registry,
476+
store,
477+
'notion-mcp',
478+
JSON.stringify({ clientId: 'a', clientSecret: 'b' })
479+
)
480+
).toThrow(PrepareInputInvalidError);
481+
expect(store.get('notion-mcp')).toBeNull();
482+
});
483+
484+
it('rejects notion-mcp input missing clientId', () => {
485+
const registry = new ServiceRegistry([NOTION_MCP]);
486+
const store = createApiCredentialStore();
487+
488+
expect(() => prepareService(registry, store, 'notion-mcp', '{}')).toThrow(
489+
PrepareInputInvalidError
490+
);
491+
expect(store.get('notion-mcp')).toBeNull();
492+
});
446493
});
447494
});

0 commit comments

Comments
 (0)