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

Viewing files at commit 6450fe9d331dCommit detailsView latest

api/mcp.ts 73 lines · 2.8 KB · typescript
1
import descopeSdk from '@descope/node-sdk';
2
import type { AuthInfo } from '@modelcontextprotocol/server';
3
import { createMcpHandler, withMcpAuth } from 'mcp-handler';
4
import { MemoryAuditStore, TrustEngine } from '../packages/core/src/index.js';
5
import { registerOpenClaspTools } from '../packages/mcp-server/src/server.js';
6
import { HostedRepository } from '../packages/persistence/src/hosted.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 = context.http?.authInfo?.extra?.operatorId;
15
  if (typeof operatorId !== 'string' || !repository) return Promise.resolve(new TrustEngine());
16
  let pending = engines.get(operatorId);
17
  if (!pending) {
18
    pending = repository.list(operatorId).then((rows) => {
19
      const store = new MemoryAuditStore();
20
      for (const row of rows) {
21
        if (row.kind !== 'interaction') store.append(row.kind, row.recordId, row.payload);
22
      }
23
      return new TrustEngine(store);
24
    });
25
    engines.set(operatorId, pending);
26
  }
27
  return pending;
28
}
29
30
const mcp = createMcpHandler(
31
  (server) =>
32
    registerOpenClaspTools(
33
      server,
34
      engineFor,
35
      repository
36
        ? (operatorId, kind, recordId, value) =>
37
            repository.upsert(operatorId, kind, recordId, value)
38
        : undefined,
39
    ),
40
  {
41
    serverInfo: { name: 'openclasp', version: '0.1.0' },
42
    instructions:
43
      'OpenClasp provides identity, interaction assurance, private clues, receipts, and contextual behavioural history. Never submit raw private conversations.',
44
  },
45
);
46
47
async function verifyToken(_request: Request, bearerToken?: string): Promise<AuthInfo | undefined> {
48
  const projectId = process.env.DESCOPE_PROJECT_ID ?? process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID;
49
  if (!projectId || !bearerToken) return undefined;
50
  const descope = descopeSdk({ projectId });
51
  const authentication = await descope.validateSession(bearerToken, {
52
    ...(process.env.OPENCLASP_MCP_URL ? { audience: process.env.OPENCLASP_MCP_URL } : {}),
53
  });
54
  if (!authentication.token.exp) return undefined;
55
  return {
56
    token: bearerToken,
57
    clientId: authentication.token.sub ?? 'unknown-client',
58
    scopes: descope.getJwtPermissions(bearerToken),
59
    expiresAt: authentication.token.exp,
60
    extra: { operatorId: authentication.token.sub },
61
  };
62
}
63
64
const handler = withMcpAuth(mcp, verifyToken, {
65
  required: true,
66
  requiredScopes: ['full-access'],
67
  resourceMetadataPath: '/.well-known/oauth-protected-resource',
68
  ...(process.env.OPENCLASP_MCP_URL
69
    ? { resourceUrl: new URL(process.env.OPENCLASP_MCP_URL).origin }
70
    : {}),
71
});
72
73
export { handler as GET, handler as POST };