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

Viewing files at commit bd8b77ebf65aCommit detailsView latest

packages/mcp-server/src/server.ts 278 lines · 8.8 KB · typescript
1
import { McpServer } from '@modelcontextprotocol/server';
2
import { z } from 'zod';
3
import { createIdentity, FixtureFactCheckProvider, TrustEngine } from '../../core/src/index.js';
4
import {
5
  AgentIdentitySchema,
6
  FeedbackSchema,
7
  InteractionEventSchema,
8
  ReceiptSchema,
9
} from '../../protocol/src/index.js';
10
11
export const OPENCLASP_TOOL_NAMES = [
12
  'openclasp_create_identity',
13
  'openclasp_register_agent',
14
  'openclasp_get_profile',
15
  'openclasp_assess_counterparty',
16
  'openclasp_begin_interaction',
17
  'openclasp_record_event',
18
  'openclasp_check_claim',
19
  'openclasp_validate_commitment',
20
  'openclasp_suggest_resolution',
21
  'openclasp_complete_interaction',
22
  'openclasp_submit_feedback',
23
  'openclasp_raise_dispute',
24
  'openclasp_verify_receipt',
25
] as const;
26
27
const text = (value: unknown) => ({
28
  content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
29
});
30
31
export function buildMcpServer(engine = new TrustEngine()) {
32
  const server = new McpServer(
33
    { name: 'openclasp', version: '0.1.0' },
34
    {
35
      instructions:
36
        'Use OpenClasp to verify counterparties, record signed structured events, and request contextual risk decisions. Never send raw private conversations.',
37
    },
38
  );
39
  return registerOpenClaspTools(server, engine);
40
}
41
42
type HostedRecord = (
43
  operatorId: string,
44
  kind: 'agent' | 'interaction' | 'event' | 'receipt' | 'feedback' | 'conflict' | 'profile',
45
  recordId: string,
46
  value: unknown,
47
) => Promise<void>;
48
49
type ToolContext = { http?: { authInfo?: { extra?: Record<string, unknown> } } };
50
type EngineSource = TrustEngine | ((context: ToolContext) => Promise<TrustEngine>);
51
52
export function registerOpenClaspTools(
53
  server: McpServer,
54
  engineSource: EngineSource = new TrustEngine(),
55
  recordHosted?: HostedRecord,
56
) {
57
  const factChecker = new FixtureFactCheckProvider();
58
  const resolveEngine = (context: ToolContext) =>
59
    typeof engineSource === 'function' ? engineSource(context) : Promise.resolve(engineSource);
60
  const persist = async (
61
    context: ToolContext,
62
    kind: Parameters<HostedRecord>[1],
63
    id: string,
64
    value: unknown,
65
  ) => {
66
    const operatorId = context.http?.authInfo?.extra?.operatorId;
67
    if (recordHosted && typeof operatorId === 'string')
68
      await recordHosted(operatorId, kind, id, value);
69
  };
70
71
  server.registerTool(
72
    OPENCLASP_TOOL_NAMES[0],
73
    {
74
      description: 'Create a local Ed25519 agent identity',
75
      inputSchema: z.object({
76
        agentId: z.string(),
77
        operatorRef: z.string(),
78
        capabilities: z.array(z.string()),
79
      }),
80
    },
81
    async (input, context) => {
82
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
83
      const created = createIdentity({
84
        ...input,
85
        operatorRef:
86
          typeof authenticatedOperator === 'string'
87
            ? `clerk:${authenticatedOperator}`
88
            : input.operatorRef,
89
      });
90
      return text(created);
91
    },
92
  );
93
  server.registerTool(
94
    OPENCLASP_TOOL_NAMES[1],
95
    {
96
      description: 'Register and verify a signed agent identity',
97
      inputSchema: AgentIdentitySchema,
98
    },
99
    async (input, context) => {
100
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
101
      if (
102
        typeof authenticatedOperator === 'string' &&
103
        input.operatorRef !== `clerk:${authenticatedOperator}`
104
      ) {
105
        throw new Error('Agent identity is not owned by the authenticated operator');
106
      }
107
      const engine = await resolveEngine(context);
108
      const registered = engine.registerAgent(input);
109
      await persist(context, 'agent', registered.agentId, registered);
110
      return text(registered);
111
    },
112
  );
113
  server.registerTool(
114
    OPENCLASP_TOOL_NAMES[2],
115
    {
116
      description: 'Get a task-specific behavioural profile',
117
      inputSchema: z.object({ agentId: z.string(), version: z.string(), taskCategory: z.string() }),
118
    },
119
    async (input, context) => {
120
      const engine = await resolveEngine(context);
121
      return text(engine.getRisk(input.agentId, input.version, input.taskCategory));
122
    },
123
  );
124
  server.registerTool(
125
    OPENCLASP_TOOL_NAMES[3],
126
    {
127
      description: 'Assess a counterparty in context',
128
      inputSchema: z.object({
129
        envelope: z.any(),
130
        action: z.string(),
131
        dataClasses: z.array(z.string()).optional(),
132
        humanApproved: z.boolean().optional(),
133
      }),
134
    },
135
    async (input, context) => {
136
      const engine = await resolveEngine(context);
137
      return text(engine.assess(input as any));
138
    },
139
  );
140
  server.registerTool(
141
    OPENCLASP_TOOL_NAMES[4],
142
    {
143
      description: 'Begin an interaction and return its identifier',
144
      inputSchema: z.object({
145
        purpose: z.string(),
146
        parties: z.array(z.string()),
147
        taskCategory: z.string(),
148
      }),
149
    },
150
    async (input, context) => {
151
      const interaction = {
152
        interactionId: crypto.randomUUID(),
153
        status: 'contract_required',
154
        createdAt: new Date().toISOString(),
155
        ...input,
156
      };
157
      await persist(context, 'interaction', interaction.interactionId, interaction);
158
      return text(interaction);
159
    },
160
  );
161
  server.registerTool(
162
    OPENCLASP_TOOL_NAMES[5],
163
    {
164
      description: 'Validate and record a signed structured event',
165
      inputSchema: InteractionEventSchema,
166
    },
167
    async (input, context) => {
168
      const engine = await resolveEngine(context);
169
      const event = engine.recordEvent(input);
170
      await persist(context, 'event', event.eventId, event);
171
      return text(event);
172
    },
173
  );
174
  server.registerTool(
175
    OPENCLASP_TOOL_NAMES[6],
176
    {
177
      description: 'Check an objective factual claim',
178
      inputSchema: z.object({ claim: z.string(), permission: z.boolean().optional() }),
179
    },
180
    async (input) => text(await factChecker.check(input.claim, input.permission)),
181
  );
182
  server.registerTool(
183
    OPENCLASP_TOOL_NAMES[7],
184
    {
185
      description: 'Check whether a commitment is represented by a signed event',
186
      inputSchema: z.object({ interactionId: z.string().uuid(), commitment: z.string() }),
187
    },
188
    async (input, context) => {
189
      const engine = await resolveEngine(context);
190
      return text({
191
        valid: [...engine.events.values()].some(
192
          (event) =>
193
            event.interactionId === input.interactionId &&
194
            event.eventType === 'commitment' &&
195
            JSON.stringify(event.payload).includes(input.commitment),
196
        ),
197
      });
198
    },
199
  );
200
  server.registerTool(
201
    OPENCLASP_TOOL_NAMES[8],
202
    {
203
      description: 'Suggest attributable resolutions for a consented conflict',
204
      inputSchema: z.object({ conflictId: z.string() }),
205
    },
206
    async (input, context) => {
207
      const engine = await resolveEngine(context);
208
      return text(engine.conflicts.get(input.conflictId)?.possibleResolutions ?? []);
209
    },
210
  );
211
  server.registerTool(
212
    OPENCLASP_TOOL_NAMES[9],
213
    { description: 'Submit a signed completion receipt', inputSchema: ReceiptSchema },
214
    async (input, context) => {
215
      const engine = await resolveEngine(context);
216
      const receipt = engine.submitReceipt(input);
217
      await persist(context, 'receipt', receipt.receiptId, receipt);
218
      return text(receipt);
219
    },
220
  );
221
  server.registerTool(
222
    OPENCLASP_TOOL_NAMES[10],
223
    { description: 'Submit receipt-backed structured feedback', inputSchema: FeedbackSchema },
224
    async (input, context) => {
225
      const engine = await resolveEngine(context);
226
      const result = engine.submitFeedback(input);
227
      await persist(context, 'feedback', input.feedbackId, input);
228
      for (const profile of engine.profiles.values())
229
        await persist(
230
          context,
231
          'profile',
232
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
233
          profile,
234
        );
235
      return text(result);
236
    },
237
  );
238
  server.registerTool(
239
    OPENCLASP_TOOL_NAMES[11],
240
    {
241
      description: 'Create a mutually consented dispute',
242
      inputSchema: z.object({
243
        interactionId: z.string().uuid(),
244
        issue: z.string(),
245
        participants: z.array(z.string()).min(2),
246
      }),
247
    },
248
    async (input, context) => {
249
      const engine = await resolveEngine(context);
250
      const conflict = engine.createConflict({
251
        interactionId: input.interactionId,
252
        issue: input.issue,
253
        participants: input.participants,
254
        positions: {},
255
        evidence: [],
256
        contractClauses: [],
257
        missingInformation: [],
258
        possibleResolutions: [],
259
      });
260
      await persist(context, 'conflict', conflict.conflictId, conflict);
261
      return text(conflict);
262
    },
263
  );
264
  server.registerTool(
265
    OPENCLASP_TOOL_NAMES[12],
266
    { description: 'Verify a signed interaction receipt', inputSchema: ReceiptSchema },
267
    async (input, context) => {
268
      const engine = await resolveEngine(context);
269
      try {
270
        engine.submitReceipt(input);
271
        return text({ valid: true });
272
      } catch {
273
        return text({ valid: false });
274
      }
275
    },
276
  );
277
  return server;
278
}