feat(server): add request-time OAuth scope challenges (#1624)
Some checks failed
Conformance Tests / client-conformance (push) Has been cancelled
Conformance Tests / server-conformance (push) Has been cancelled
Deploy Docs / deploy-docs (push) Has been cancelled
Examples / examples (build + e2e) (push) Has been cancelled
/ build (push) Has been cancelled
/ test (20) (push) Has been cancelled
/ test (22) (push) Has been cancelled
/ test (24) (push) Has been cancelled
/ test-e2e (20) (push) Has been cancelled
/ test-e2e (22) (push) Has been cancelled
/ test-e2e (24) (push) Has been cancelled
/ test-runtimes (bun, 1.x) (push) Has been cancelled
/ test-runtimes (deno, v2.x) (push) Has been cancelled
Publish Any Commit / pkg-publish (push) Has been cancelled
Release / Version (push) Has been cancelled
Release / Publish (push) Has been cancelled
Some checks failed
Conformance Tests / client-conformance (push) Has been cancelled
Conformance Tests / server-conformance (push) Has been cancelled
Deploy Docs / deploy-docs (push) Has been cancelled
Examples / examples (build + e2e) (push) Has been cancelled
/ build (push) Has been cancelled
/ test (20) (push) Has been cancelled
/ test (22) (push) Has been cancelled
/ test (24) (push) Has been cancelled
/ test-e2e (20) (push) Has been cancelled
/ test-e2e (22) (push) Has been cancelled
/ test-e2e (24) (push) Has been cancelled
/ test-runtimes (bun, 1.x) (push) Has been cancelled
/ test-runtimes (deno, v2.x) (push) Has been cancelled
Publish Any Commit / pkg-publish (push) Has been cancelled
Release / Version (push) Has been cancelled
Release / Publish (push) Has been cancelled
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b65426158e
commit
6032170087
1 changed files with 1310 additions and 104 deletions
|
|
@ -22,7 +22,8 @@ import {
|
|||
} from '@modelcontextprotocol/express';
|
||||
import { toNodeHandler } from '@modelcontextprotocol/node';
|
||||
import type { AuthInfo, OAuthMetadata } from '@modelcontextprotocol/server';
|
||||
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
|
||||
import { createMcpHandler, McpServer, requireScopes } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const mcpServerUrl = new URL('https://api.example.com/mcp');
|
||||
const verifier: OAuthTokenVerifier = { verifyAccessToken };
|
||||
|
|
@ -72,14 +73,36 @@ function buildServer(): McpServer {
|
|||
});
|
||||
//#endregion authInfo_handler
|
||||
|
||||
//#region perToolScopes_handler
|
||||
server.registerTool('purge-notes', { description: 'Delete every note' }, async ctx => {
|
||||
if (!ctx.http?.authInfo?.scopes.includes('notes:write')) {
|
||||
return { content: [{ type: 'text', text: 'insufficient_scope: purge-notes requires notes:write' }], isError: true };
|
||||
}
|
||||
return { content: [{ type: 'text', text: 'All notes deleted' }] };
|
||||
});
|
||||
//#endregion perToolScopes_handler
|
||||
//#region perOperationScopes_challenge
|
||||
server.registerTool('purge-notes', { scopeChallenge: requireScopes('notes:write') }, async () => ({
|
||||
content: [{ type: 'text', text: 'All notes deleted' }]
|
||||
}));
|
||||
|
||||
server.registerResource('private-notes', 'notes://private', { scopeChallenge: requireScopes('notes:read') }, async uri => ({
|
||||
contents: [{ uri: uri.href, text: 'Private notes' }]
|
||||
}));
|
||||
|
||||
server.registerPrompt('summarize-notes', { scopeChallenge: requireScopes('notes:read') }, async () => ({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: 'Summarize my private notes' } }]
|
||||
}));
|
||||
|
||||
server.registerTool(
|
||||
'read-repository',
|
||||
{
|
||||
inputSchema: z.object({ visibility: z.enum(['public', 'private']) }),
|
||||
scopeChallenge: ({ request, authInfo }) => {
|
||||
const visibility = (request.params as { arguments?: { visibility?: unknown } }).arguments?.visibility;
|
||||
if (visibility !== 'public' && visibility !== 'private') return;
|
||||
|
||||
const scopes = visibility === 'private' ? (['repo:read'] as const) : (['public_repo'] as const);
|
||||
return scopes.every(scope => authInfo?.scopes.includes(scope))
|
||||
? undefined
|
||||
: { scopes, errorDescription: `${visibility} repository access is required` };
|
||||
}
|
||||
},
|
||||
async ({ visibility }) => ({ content: [{ type: 'text', text: `Read ${visibility} repository` }] })
|
||||
);
|
||||
//#endregion perOperationScopes_challenge
|
||||
|
||||
return server;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue