| 1 | import { createClerkClient } from '@clerk/backend';
|
| 2 | import { verifyClerkToken } from '@clerk/mcp-tools/next';
|
| 3 | import type { AuthInfo } from '@modelcontextprotocol/server';
|
| 4 | import { createMcpHandler, withMcpAuth } from 'mcp-handler';
|
| 5 | import { MemoryAuditStore, TrustEngine } from '../packages/core/src/index.js';
|
| 6 | import { registerOpenClaspTools } from '../packages/mcp-server/src/server.js';
|
| 7 | import { HostedRepository } from '../packages/persistence/src/hosted.js';
|
| 8 |
|
| 9 | const repository = process.env.DATABASE_URL
|
| 10 | ? new HostedRepository(process.env.DATABASE_URL)
|
| 11 | : undefined;
|
| 12 | const engines = new Map<string, Promise<TrustEngine>>();
|
| 13 |
|
| 14 | function engineFor(context: { http?: { authInfo?: { extra?: Record<string, unknown> } } }) {
|
| 15 | const operatorId =
|
| 16 | context.http?.authInfo?.extra?.operatorId ?? context.http?.authInfo?.extra?.userId;
|
| 17 | if (typeof operatorId !== 'string' || !repository) return Promise.resolve(new TrustEngine());
|
| 18 | let pending = engines.get(operatorId);
|
| 19 | if (!pending) {
|
| 20 | pending = repository.list(operatorId).then((rows) => {
|
| 21 | const store = new MemoryAuditStore();
|
| 22 | for (const row of rows) {
|
| 23 | if (row.kind !== 'interaction') store.append(row.kind, row.recordId, row.payload);
|
| 24 | }
|
| 25 | return new TrustEngine(store);
|
| 26 | });
|
| 27 | engines.set(operatorId, pending);
|
| 28 | }
|
| 29 | return pending;
|
| 30 | }
|
| 31 |
|
| 32 | const mcp = createMcpHandler(
|
| 33 | (server) =>
|
| 34 | registerOpenClaspTools(
|
| 35 | server,
|
| 36 | engineFor,
|
| 37 | repository
|
| 38 | ? (operatorId, kind, recordId, value) =>
|
| 39 | repository.upsert(operatorId, kind, recordId, value)
|
| 40 | : undefined,
|
| 41 | ),
|
| 42 | {
|
| 43 | serverInfo: { name: 'openclasp', version: '0.1.0' },
|
| 44 | instructions:
|
| 45 | 'OpenClasp provides identity, interaction assurance, private clues, receipts, and contextual behavioural history. Never submit raw private conversations.',
|
| 46 | },
|
| 47 | );
|
| 48 |
|
| 49 | async function verifyToken(request: Request, bearerToken?: string): Promise<AuthInfo | undefined> {
|
| 50 | const secretKey = process.env.CLERK_SECRET_KEY;
|
| 51 | const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
| 52 | if (!secretKey || !publishableKey || !bearerToken) return undefined;
|
| 53 | const state = await createClerkClient({ secretKey, publishableKey }).authenticateRequest(
|
| 54 | request,
|
| 55 | {
|
| 56 | acceptsToken: 'oauth_token',
|
| 57 | },
|
| 58 | );
|
| 59 | if (!state.isAuthenticated) return undefined;
|
| 60 | const authInfo = verifyClerkToken(state.toAuth(), bearerToken);
|
| 61 | if (!authInfo) return undefined;
|
| 62 | return { ...authInfo, extra: { ...authInfo.extra, operatorId: state.toAuth().userId } };
|
| 63 | }
|
| 64 |
|
| 65 | const handler = withMcpAuth(mcp, verifyToken, {
|
| 66 | required: true,
|
| 67 | requiredScopes: ['profile'],
|
| 68 | resourceMetadataPath: '/.well-known/oauth-protected-resource',
|
| 69 | ...(process.env.OPENCLASP_MCP_URL
|
| 70 | ? { resourceUrl: new URL(process.env.OPENCLASP_MCP_URL).origin }
|
| 71 | : {}),
|
| 72 | });
|
| 73 |
|
| 74 | export { handler as GET, handler as POST };
|