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

Viewing files at commit a93dad91899fCommit detailsView latest

api/mcp.ts 74 lines · 2.6 KB · typescript
1
import type { AuthInfo } from '@modelcontextprotocol/server';
2
import { createMcpHandler, withMcpAuth } from 'mcp-handler';
3
import { MemoryAuditStore, TrustEngine } from '../packages/core/src/index.js';
4
import { registerOpenClaspTools } from '../packages/mcp-server/src/server.js';
5
import { HostedRepository } from '../packages/persistence/src/hosted.js';
6
import { verifyAuth0Token } from './auth0.js';
7
8
const repository = process.env.DATABASE_URL
9
  ? new HostedRepository(process.env.DATABASE_URL)
10
  : undefined;
11
const engines = new Map<string, Promise<TrustEngine>>();
12
13
function engineFor(context: { http?: { authInfo?: { extra?: Record<string, unknown> } } }) {
14
  const operatorId =
15
    context.http?.authInfo?.extra?.operatorId ?? context.http?.authInfo?.extra?.userId;
16
  if (typeof operatorId !== 'string' || !repository) return Promise.resolve(new TrustEngine());
17
  let pending = engines.get(operatorId);
18
  if (!pending) {
19
    pending = repository.list(operatorId).then((rows) => {
20
      const store = new MemoryAuditStore();
21
      for (const row of rows) {
22
        if (row.kind !== 'interaction') store.append(row.kind, row.recordId, row.payload);
23
      }
24
      return new TrustEngine(store);
25
    });
26
    engines.set(operatorId, pending);
27
  }
28
  return pending;
29
}
30
31
const mcp = createMcpHandler(
32
  (server) =>
33
    registerOpenClaspTools(
34
      server,
35
      engineFor,
36
      repository
37
        ? (operatorId, kind, recordId, value) =>
38
            repository.upsert(operatorId, kind, recordId, value)
39
        : undefined,
40
    ),
41
  {
42
    serverInfo: { name: 'openclasp', version: '0.1.0' },
43
    instructions:
44
      'OpenClasp provides identity, interaction assurance, private clues, receipts, and contextual behavioural history. Never submit raw private conversations.',
45
  },
46
);
47
48
async function verifyToken(_request: Request, bearerToken?: string): Promise<AuthInfo | undefined> {
49
  if (!bearerToken) return undefined;
50
  const authentication = await verifyAuth0Token(bearerToken, {
51
    dashboard: false,
52
    requiredScopes: ['mcp:access'],
53
  });
54
  return {
55
    token: bearerToken,
56
    clientId: authentication.clientId,
57
    scopes: authentication.scopes,
58
    ...(typeof authentication.payload.exp === 'number'
59
      ? { expiresAt: authentication.payload.exp }
60
      : {}),
61
    extra: { operatorId: authentication.payload.sub },
62
  };
63
}
64
65
const handler = withMcpAuth(mcp, verifyToken, {
66
  required: true,
67
  requiredScopes: ['mcp:access'],
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 };