The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit bd8b77ebf65aCommit detailsView latest

api/mcp.ts 74 lines · 2.8 KB · typescript
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 };