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

Viewing files at commit 3dc3c2c6da63Commit detailsView latest

packages/mcp-server/src/server.ts 437 lines · 14.6 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
import {
11
  requestAgentSetup,
12
  resolveInstallation,
13
  updateAgentProfile,
14
  type OnboardingStore,
15
} from '../../persistence/src/onboarding.js';
16
17
export const OPENCLASP_TOOL_NAMES = [
18
  'openclasp_create_identity',
19
  'openclasp_register_agent',
20
  'openclasp_get_profile',
21
  'openclasp_assess_counterparty',
22
  'openclasp_begin_interaction',
23
  'openclasp_record_event',
24
  'openclasp_check_claim',
25
  'openclasp_validate_commitment',
26
  'openclasp_suggest_resolution',
27
  'openclasp_complete_interaction',
28
  'openclasp_submit_feedback',
29
  'openclasp_raise_dispute',
30
  'openclasp_verify_receipt',
31
  'openclasp_setup',
32
  'openclasp_get_identity',
33
  'openclasp_switch_agent',
34
  'openclasp_update_profile',
35
  'openclasp_connection_status',
36
] as const;
37
38
const text = (value: unknown) => ({
39
  content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
40
});
41
42
export function buildMcpServer(engine = new TrustEngine()) {
43
  const server = new McpServer(
44
    { name: 'openclasp', version: '0.1.0' },
45
    {
46
      instructions:
47
        'Use OpenClasp to verify counterparties, record signed structured events, and request contextual risk decisions. Never send raw private conversations.',
48
    },
49
  );
50
  return registerOpenClaspTools(server, engine);
51
}
52
53
type HostedRecord = (
54
  operatorId: string,
55
  kind: 'agent' | 'interaction' | 'event' | 'receipt' | 'feedback' | 'conflict' | 'profile',
56
  recordId: string,
57
  value: unknown,
58
) => Promise<void>;
59
60
type ToolContext = {
61
  http?: {
62
    authInfo?: { clientId?: string; extra?: Record<string, unknown> };
63
  };
64
};
65
type EngineSource = TrustEngine | ((context: ToolContext) => Promise<TrustEngine>);
66
67
function installationContext(context: ToolContext) {
68
  const operatorId = context.http?.authInfo?.extra?.operatorId;
69
  const clientId = context.http?.authInfo?.clientId;
70
  if (typeof operatorId !== 'string' || typeof clientId !== 'string')
71
    throw new Error('Authenticated MCP installation context is required');
72
  return { operatorId, clientId };
73
}
74
75
export function registerOpenClaspTools(
76
  server: McpServer,
77
  engineSource: EngineSource = new TrustEngine(),
78
  recordHosted?: HostedRecord,
79
  onboardingStore?: OnboardingStore,
80
) {
81
  const factChecker = new FixtureFactCheckProvider();
82
  const resolveEngine = (context: ToolContext) =>
83
    typeof engineSource === 'function' ? engineSource(context) : Promise.resolve(engineSource);
84
  const persist = async (
85
    context: ToolContext,
86
    kind: Parameters<HostedRecord>[1],
87
    id: string,
88
    value: unknown,
89
  ) => {
90
    const operatorId = context.http?.authInfo?.extra?.operatorId;
91
    if (recordHosted && typeof operatorId === 'string')
92
      await recordHosted(operatorId, kind, id, value);
93
  };
94
95
  server.registerTool(
96
    OPENCLASP_TOOL_NAMES[0],
97
    {
98
      description: 'Create a local Ed25519 agent identity',
99
      inputSchema: z.object({
100
        agentId: z.string(),
101
        operatorRef: z.string(),
102
        capabilities: z.array(z.string()),
103
      }),
104
    },
105
    async (input, context) => {
106
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
107
      const created = createIdentity({
108
        ...input,
109
        operatorRef:
110
          typeof authenticatedOperator === 'string'
111
            ? `auth0:${authenticatedOperator}`
112
            : input.operatorRef,
113
      });
114
      return text(created);
115
    },
116
  );
117
  server.registerTool(
118
    OPENCLASP_TOOL_NAMES[1],
119
    {
120
      description: 'Register and verify a signed agent identity',
121
      inputSchema: AgentIdentitySchema,
122
    },
123
    async (input, context) => {
124
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
125
      if (
126
        typeof authenticatedOperator === 'string' &&
127
        input.operatorRef !== `auth0:${authenticatedOperator}`
128
      ) {
129
        throw new Error('Agent identity is not owned by the authenticated operator');
130
      }
131
      const engine = await resolveEngine(context);
132
      const registered = engine.registerAgent(input);
133
      await persist(context, 'agent', registered.agentId, registered);
134
      return text(registered);
135
    },
136
  );
137
  server.registerTool(
138
    OPENCLASP_TOOL_NAMES[2],
139
    {
140
      description: 'Get a task-specific behavioural profile',
141
      inputSchema: z.object({ agentId: z.string(), version: z.string(), taskCategory: z.string() }),
142
    },
143
    async (input, context) => {
144
      const engine = await resolveEngine(context);
145
      return text(engine.getRisk(input.agentId, input.version, input.taskCategory));
146
    },
147
  );
148
  server.registerTool(
149
    OPENCLASP_TOOL_NAMES[3],
150
    {
151
      description: 'Assess a counterparty in context',
152
      inputSchema: z.object({
153
        envelope: z.any(),
154
        action: z.string(),
155
        dataClasses: z.array(z.string()).optional(),
156
        humanApproved: z.boolean().optional(),
157
      }),
158
    },
159
    async (input, context) => {
160
      const engine = await resolveEngine(context);
161
      return text(engine.assess(input as any));
162
    },
163
  );
164
  server.registerTool(
165
    OPENCLASP_TOOL_NAMES[4],
166
    {
167
      description: 'Begin an interaction and return its identifier',
168
      inputSchema: z.object({
169
        purpose: z.string(),
170
        parties: z.array(z.string()),
171
        taskCategory: z.string(),
172
      }),
173
    },
174
    async (input, context) => {
175
      const interaction = {
176
        interactionId: crypto.randomUUID(),
177
        status: 'contract_required',
178
        createdAt: new Date().toISOString(),
179
        ...input,
180
      };
181
      await persist(context, 'interaction', interaction.interactionId, interaction);
182
      return text(interaction);
183
    },
184
  );
185
  server.registerTool(
186
    OPENCLASP_TOOL_NAMES[5],
187
    {
188
      description: 'Validate and record a signed structured event',
189
      inputSchema: InteractionEventSchema,
190
    },
191
    async (input, context) => {
192
      const engine = await resolveEngine(context);
193
      const event = engine.recordEvent(input);
194
      await persist(context, 'event', event.eventId, event);
195
      return text(event);
196
    },
197
  );
198
  server.registerTool(
199
    OPENCLASP_TOOL_NAMES[6],
200
    {
201
      description: 'Check an objective factual claim',
202
      inputSchema: z.object({ claim: z.string(), permission: z.boolean().optional() }),
203
    },
204
    async (input) => text(await factChecker.check(input.claim, input.permission)),
205
  );
206
  server.registerTool(
207
    OPENCLASP_TOOL_NAMES[7],
208
    {
209
      description: 'Check whether a commitment is represented by a signed event',
210
      inputSchema: z.object({ interactionId: z.string().uuid(), commitment: z.string() }),
211
    },
212
    async (input, context) => {
213
      const engine = await resolveEngine(context);
214
      return text({
215
        valid: [...engine.events.values()].some(
216
          (event) =>
217
            event.interactionId === input.interactionId &&
218
            event.eventType === 'commitment' &&
219
            JSON.stringify(event.payload).includes(input.commitment),
220
        ),
221
      });
222
    },
223
  );
224
  server.registerTool(
225
    OPENCLASP_TOOL_NAMES[8],
226
    {
227
      description: 'Suggest attributable resolutions for a consented conflict',
228
      inputSchema: z.object({ conflictId: z.string() }),
229
    },
230
    async (input, context) => {
231
      const engine = await resolveEngine(context);
232
      return text(engine.conflicts.get(input.conflictId)?.possibleResolutions ?? []);
233
    },
234
  );
235
  server.registerTool(
236
    OPENCLASP_TOOL_NAMES[9],
237
    { description: 'Submit a signed completion receipt', inputSchema: ReceiptSchema },
238
    async (input, context) => {
239
      const engine = await resolveEngine(context);
240
      const receipt = engine.submitReceipt(input);
241
      await persist(context, 'receipt', receipt.receiptId, receipt);
242
      return text(receipt);
243
    },
244
  );
245
  server.registerTool(
246
    OPENCLASP_TOOL_NAMES[10],
247
    { description: 'Submit receipt-backed structured feedback', inputSchema: FeedbackSchema },
248
    async (input, context) => {
249
      const engine = await resolveEngine(context);
250
      const result = engine.submitFeedback(input);
251
      await persist(context, 'feedback', input.feedbackId, input);
252
      for (const profile of engine.profiles.values())
253
        await persist(
254
          context,
255
          'profile',
256
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
257
          profile,
258
        );
259
      return text(result);
260
    },
261
  );
262
  server.registerTool(
263
    OPENCLASP_TOOL_NAMES[11],
264
    {
265
      description: 'Create a mutually consented dispute',
266
      inputSchema: z.object({
267
        interactionId: z.string().uuid(),
268
        issue: z.string(),
269
        participants: z.array(z.string()).min(2),
270
      }),
271
    },
272
    async (input, context) => {
273
      const engine = await resolveEngine(context);
274
      const conflict = engine.createConflict({
275
        interactionId: input.interactionId,
276
        issue: input.issue,
277
        participants: input.participants,
278
        positions: {},
279
        evidence: [],
280
        contractClauses: [],
281
        missingInformation: [],
282
        possibleResolutions: [],
283
      });
284
      await persist(context, 'conflict', conflict.conflictId, conflict);
285
      return text(conflict);
286
    },
287
  );
288
  server.registerTool(
289
    OPENCLASP_TOOL_NAMES[12],
290
    { description: 'Verify a signed interaction receipt', inputSchema: ReceiptSchema },
291
    async (input, context) => {
292
      const engine = await resolveEngine(context);
293
      try {
294
        engine.submitReceipt(input);
295
        return text({ valid: true });
296
      } catch {
297
        return text({ valid: false });
298
      }
299
    },
300
  );
301
  server.registerTool(
302
    OPENCLASP_TOOL_NAMES[13],
303
    {
304
      description:
305
        'Propose this MCP installation as a new OpenClasp agent. The owner must confirm it in the dashboard before the identity is bound.',
306
      inputSchema: z
307
        .object({
308
          agentName: z.string().trim().min(1).max(100),
309
          projectName: z.string().trim().min(1).max(100).optional(),
310
          projectId: z.string().optional(),
311
          framework: z.string().trim().max(100).optional(),
312
          capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
313
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
314
        })
315
        .refine((input) => input.projectName || input.projectId, {
316
          message: 'projectName or projectId is required',
317
        }),
318
    },
319
    async (input, context) => {
320
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
321
      const connection = installationContext(context);
322
      const current = await resolveInstallation(
323
        onboardingStore,
324
        connection.operatorId,
325
        connection.clientId,
326
      );
327
      if (current.status === 'connected') return text(current);
328
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
329
        clientId: connection.clientId,
330
        ...input,
331
      });
332
      return text({
333
        status: 'pending_confirmation',
334
        confirmationRequired: true,
335
        request,
336
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
337
        next: 'Ask the owner to confirm this setup request in the OpenClasp dashboard, then call openclasp_get_identity.',
338
      });
339
    },
340
  );
341
  server.registerTool(
342
    OPENCLASP_TOOL_NAMES[14],
343
    {
344
      description:
345
        'Return the agent identity automatically bound to this authenticated MCP installation.',
346
      inputSchema: z.object({}),
347
    },
348
    async (_input, context) => {
349
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
350
      const connection = installationContext(context);
351
      return text(
352
        await resolveInstallation(onboardingStore, connection.operatorId, connection.clientId),
353
      );
354
    },
355
  );
356
  server.registerTool(
357
    OPENCLASP_TOOL_NAMES[15],
358
    {
359
      description:
360
        'Request that this MCP installation switch to another existing agent. Dashboard confirmation is required.',
361
      inputSchema: z.object({ existingAgentId: z.string().min(1) }),
362
    },
363
    async (input, context) => {
364
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
365
      const connection = installationContext(context);
366
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
367
        clientId: connection.clientId,
368
        action: 'switch',
369
        existingAgentId: input.existingAgentId,
370
      });
371
      return text({
372
        status: 'pending_confirmation',
373
        confirmationRequired: true,
374
        request,
375
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
376
      });
377
    },
378
  );
379
  server.registerTool(
380
    OPENCLASP_TOOL_NAMES[16],
381
    {
382
      description: 'Update the profile of the agent bound to this MCP installation.',
383
      inputSchema: z.object({
384
        name: z.string().trim().min(1).max(100).optional(),
385
        framework: z.string().trim().min(1).max(100).optional(),
386
        capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
387
        limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
388
      }),
389
    },
390
    async (input, context) => {
391
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
392
      const connection = installationContext(context);
393
      return text(
394
        await updateAgentProfile(
395
          onboardingStore,
396
          connection.operatorId,
397
          connection.clientId,
398
          input,
399
        ),
400
      );
401
    },
402
  );
403
  server.registerTool(
404
    OPENCLASP_TOOL_NAMES[17],
405
    {
406
      description:
407
        'Check whether this MCP installation is unbound, awaiting confirmation, or connected.',
408
      inputSchema: z.object({}),
409
    },
410
    async (_input, context) => {
411
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
412
      const connection = installationContext(context);
413
      const binding = await resolveInstallation(
414
        onboardingStore,
415
        connection.operatorId,
416
        connection.clientId,
417
      );
418
      if (binding.status === 'connected') return text(binding);
419
      const state = await onboardingStore.list(connection.operatorId);
420
      const pending = state
421
        .filter((row) => row.kind === 'setup_request')
422
        .map((row) => row.payload as { clientId?: string; status?: string })
423
        .find(
424
          (request) => request.clientId === connection.clientId && request.status === 'pending',
425
        );
426
      return text(
427
        pending
428
          ? {
429
              status: 'pending_confirmation',
430
              confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
431
            }
432
          : binding,
433
      );
434
    },
435
  );
436
  return server;
437
}