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

Viewing files at commit 132dee380109Commit detailsView latest

packages/mcp-server/src/server.ts 908 lines · 33.9 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
  DEFAULT_EXTENSION_URI,
7
  DelegationCredentialSchema,
8
  FeedbackSchema,
9
  InteractionContractSchema,
10
  InteractionEventSchema,
11
  ReceiptSchema,
12
  TrustEnvelopeSchema,
13
  canonicalHash,
14
  type FederatedInteraction,
15
  type PublicAgentCard,
16
} from '../../protocol/src/index.js';
17
import {
18
  requestAgentSetup,
19
  resolveInstallation,
20
  updateAgentProfile,
21
  type OnboardingStore,
22
} from '../../persistence/src/onboarding.js';
23
import { buildPublicAgentCard } from '../../persistence/src/hosted.js';
24
25
export const OPENCLASP_TOOL_NAMES = [
26
  'openclasp_create_identity',
27
  'openclasp_register_agent',
28
  'openclasp_get_profile',
29
  'openclasp_assess_counterparty',
30
  'openclasp_begin_interaction',
31
  'openclasp_record_event',
32
  'openclasp_check_claim',
33
  'openclasp_validate_commitment',
34
  'openclasp_suggest_resolution',
35
  'openclasp_complete_interaction',
36
  'openclasp_submit_feedback',
37
  'openclasp_raise_dispute',
38
  'openclasp_verify_receipt',
39
  'openclasp_setup',
40
  'openclasp_get_identity',
41
  'openclasp_switch_agent',
42
  'openclasp_update_profile',
43
  'openclasp_connection_status',
44
  'openclasp_register_delegation',
45
  'openclasp_save_contract',
46
  'openclasp_permit_mediation',
47
  'openclasp_resolve_dispute',
48
  'openclasp_find_agent',
49
  'openclasp_search_agents',
50
  'openclasp_connect_to_agent',
51
  'openclasp_list_invitations',
52
  'openclasp_respond_invitation',
53
  'openclasp_get_shared_interaction',
54
] as const;
55
56
export const HOSTED_OPENCLASP_TOOL_NAMES = OPENCLASP_TOOL_NAMES.filter(
57
  (name) => name !== 'openclasp_create_identity',
58
);
59
60
const READ_ONLY_TOOL = {
61
  readOnlyHint: true,
62
  destructiveHint: false,
63
  idempotentHint: true,
64
} as const;
65
const WRITE_TOOL = {
66
  readOnlyHint: false,
67
  destructiveHint: false,
68
  idempotentHint: false,
69
} as const;
70
71
const text = (value: unknown) => ({
72
  content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
73
});
74
75
export const OPENCLASP_MCP_INSTRUCTIONS =
76
  'Call openclasp_connection_status first. If unbound, use openclasp_setup and get owner approval. Treat the bound identity as yourself. Use openclasp_connect_to_agent for a one-call A2A assurance handshake; use list/respond for invitations. Never claim another identity or upload raw conversations. OpenClasp assures A2A; it is not the message transport.';
77
78
export function buildMcpServer(engine = new TrustEngine()) {
79
  const server = new McpServer(
80
    { name: 'openclasp', version: '0.1.0' },
81
    {
82
      instructions: OPENCLASP_MCP_INSTRUCTIONS,
83
    },
84
  );
85
  return registerOpenClaspTools(server, engine);
86
}
87
88
type HostedRecord = (
89
  operatorId: string,
90
  kind:
91
    | 'agent'
92
    | 'delegation'
93
    | 'contract'
94
    | 'interaction'
95
    | 'event'
96
    | 'receipt'
97
    | 'feedback'
98
    | 'conflict'
99
    | 'profile',
100
  recordId: string,
101
  value: unknown,
102
) => Promise<void>;
103
104
type ToolContext = {
105
  http?: {
106
    authInfo?: { clientId?: string; extra?: Record<string, unknown> };
107
  };
108
};
109
type EngineSource = TrustEngine | ((context: ToolContext) => Promise<TrustEngine>);
110
type AgentDirectory = {
111
  publishAgent?(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard>;
112
  getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined>;
113
  searchPublishedAgents(input: {
114
    query?: string | undefined;
115
    capability?: string | undefined;
116
    limit?: number | undefined;
117
  }): Promise<PublicAgentCard[]>;
118
  createFederatedInteraction(
119
    operatorId: string,
120
    value: FederatedInteraction,
121
  ): Promise<FederatedInteraction>;
122
  listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]>;
123
  getFederatedInteraction(
124
    operatorId: string,
125
    interactionId: string,
126
  ): Promise<FederatedInteraction | undefined>;
127
  respondToFederatedInteraction(
128
    operatorId: string,
129
    interactionId: string,
130
    agentId: string,
131
    decision: 'accept' | 'reject',
132
    method?: 'oauth_installation' | 'oauth_account',
133
  ): Promise<FederatedInteraction>;
134
};
135
136
function installationContext(context: ToolContext) {
137
  const operatorId = context.http?.authInfo?.extra?.operatorId;
138
  const clientId = context.http?.authInfo?.clientId;
139
  if (typeof operatorId !== 'string' || typeof clientId !== 'string')
140
    throw new Error('Authenticated MCP installation context is required');
141
  return { operatorId, clientId };
142
}
143
144
export function registerOpenClaspTools(
145
  server: McpServer,
146
  engineSource: EngineSource = new TrustEngine(),
147
  recordHosted?: HostedRecord,
148
  onboardingStore?: OnboardingStore,
149
  agentDirectory?: AgentDirectory,
150
) {
151
  const factChecker = new FixtureFactCheckProvider();
152
  const resolveEngine = (context: ToolContext) =>
153
    typeof engineSource === 'function' ? engineSource(context) : Promise.resolve(engineSource);
154
  const persist = async (
155
    context: ToolContext,
156
    kind: Parameters<HostedRecord>[1],
157
    id: string,
158
    value: unknown,
159
  ) => {
160
    const operatorId = context.http?.authInfo?.extra?.operatorId;
161
    if (recordHosted && typeof operatorId === 'string')
162
      await recordHosted(operatorId, kind, id, value);
163
  };
164
165
  const requireBoundAgent = async (context: ToolContext, claimedAgentId?: string) => {
166
    if (!onboardingStore) return undefined;
167
    const connection = installationContext(context);
168
    const binding = await resolveInstallation(
169
      onboardingStore,
170
      connection.operatorId,
171
      connection.clientId,
172
    );
173
    if (binding.status !== 'connected')
174
      throw new Error('Call openclasp_setup and obtain owner confirmation before using this tool');
175
    if (claimedAgentId && binding.agent.agentId !== claimedAgentId)
176
      throw new Error('The claimed agent does not match this MCP installation');
177
    return binding;
178
  };
179
180
  if (!onboardingStore)
181
    server.registerTool(
182
      OPENCLASP_TOOL_NAMES[0],
183
      {
184
        title: 'Create local cryptographic identity',
185
        description:
186
          'Create an Ed25519 identity for local development. This returns private key material and is intentionally unavailable on hosted MCP.',
187
        inputSchema: z.object({
188
          agentId: z.string(),
189
          operatorRef: z.string(),
190
          capabilities: z.array(z.string()),
191
        }),
192
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
193
      },
194
      async (input, context) => {
195
        const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
196
        const created = createIdentity({
197
          ...input,
198
          operatorRef:
199
            typeof authenticatedOperator === 'string'
200
              ? `auth0:${authenticatedOperator}`
201
              : input.operatorRef,
202
        });
203
        return text(created);
204
      },
205
    );
206
  server.registerTool(
207
    OPENCLASP_TOOL_NAMES[1],
208
    {
209
      title: 'Register cryptographic identity',
210
      description: 'Register and verify a signed agent identity',
211
      inputSchema: AgentIdentitySchema,
212
      annotations: { ...WRITE_TOOL, idempotentHint: true },
213
    },
214
    async (input, context) => {
215
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
216
      if (
217
        typeof authenticatedOperator === 'string' &&
218
        input.operatorRef !== `auth0:${authenticatedOperator}`
219
      ) {
220
        throw new Error('Agent identity is not owned by the authenticated operator');
221
      }
222
      await requireBoundAgent(context, input.agentId);
223
      const engine = await resolveEngine(context);
224
      const registered = engine.registerAgent(input);
225
      await persist(context, 'agent', registered.agentId, registered);
226
      return text(registered);
227
    },
228
  );
229
  server.registerTool(
230
    OPENCLASP_TOOL_NAMES[2],
231
    {
232
      title: 'Get contextual reliability',
233
      description: 'Get a task-specific behavioural profile',
234
      inputSchema: z.object({ agentId: z.string(), version: z.string(), taskCategory: z.string() }),
235
      annotations: READ_ONLY_TOOL,
236
    },
237
    async (input, context) => {
238
      await requireBoundAgent(context);
239
      const engine = await resolveEngine(context);
240
      return text(engine.getRisk(input.agentId, input.version, input.taskCategory));
241
    },
242
  );
243
  server.registerTool(
244
    OPENCLASP_TOOL_NAMES[3],
245
    {
246
      title: 'Assess counterparty',
247
      description: 'Assess a counterparty in context',
248
      inputSchema: z.object({
249
        envelope: TrustEnvelopeSchema,
250
        action: z.string(),
251
        dataClasses: z.array(z.string()).optional(),
252
        humanApproved: z.boolean().optional(),
253
      }),
254
      annotations: READ_ONLY_TOOL,
255
    },
256
    async (input, context) => {
257
      await requireBoundAgent(context);
258
      const engine = await resolveEngine(context);
259
      return text(engine.assess(input as any));
260
    },
261
  );
262
  server.registerTool(
263
    OPENCLASP_TOOL_NAMES[4],
264
    {
265
      title: 'Begin assured interaction',
266
      description: 'Begin an interaction and return its identifier',
267
      inputSchema: z.object({
268
        purpose: z.string(),
269
        parties: z.array(z.string()),
270
        taskCategory: z.string(),
271
      }),
272
      annotations: WRITE_TOOL,
273
    },
274
    async (input, context) => {
275
      const binding = await requireBoundAgent(context);
276
      if (binding && !input.parties.includes(binding.agent.agentId))
277
        throw new Error('Interaction parties must include the agent bound to this installation');
278
      const interaction = {
279
        interactionId: crypto.randomUUID(),
280
        status: 'contract_required',
281
        createdAt: new Date().toISOString(),
282
        ...input,
283
      };
284
      await persist(context, 'interaction', interaction.interactionId, interaction);
285
      return text(interaction);
286
    },
287
  );
288
  server.registerTool(
289
    OPENCLASP_TOOL_NAMES[5],
290
    {
291
      title: 'Record signed event',
292
      description: 'Validate and record a signed structured event',
293
      inputSchema: InteractionEventSchema,
294
      annotations: { ...WRITE_TOOL, idempotentHint: true },
295
    },
296
    async (input, context) => {
297
      await requireBoundAgent(context, input.agentId);
298
      const engine = await resolveEngine(context);
299
      const event = engine.recordEvent(input);
300
      await persist(context, 'event', event.eventId, event);
301
      return text(event);
302
    },
303
  );
304
  server.registerTool(
305
    OPENCLASP_TOOL_NAMES[6],
306
    {
307
      title: 'Check factual claim',
308
      description: 'Check an objective factual claim',
309
      inputSchema: z.object({ claim: z.string(), permission: z.boolean().optional() }),
310
      annotations: { ...READ_ONLY_TOOL, openWorldHint: true },
311
    },
312
    async (input) => text(await factChecker.check(input.claim, input.permission)),
313
  );
314
  server.registerTool(
315
    OPENCLASP_TOOL_NAMES[7],
316
    {
317
      title: 'Validate commitment',
318
      description: 'Check whether a commitment is represented by a signed event',
319
      inputSchema: z.object({ interactionId: z.string().uuid(), commitment: z.string() }),
320
      annotations: READ_ONLY_TOOL,
321
    },
322
    async (input, context) => {
323
      await requireBoundAgent(context);
324
      const engine = await resolveEngine(context);
325
      return text({
326
        valid: [...engine.events.values()].some(
327
          (event) =>
328
            event.interactionId === input.interactionId &&
329
            event.eventType === 'commitment' &&
330
            JSON.stringify(event.payload).includes(input.commitment),
331
        ),
332
      });
333
    },
334
  );
335
  server.registerTool(
336
    OPENCLASP_TOOL_NAMES[8],
337
    {
338
      title: 'Suggest dispute resolution',
339
      description: 'Suggest attributable resolutions for a consented conflict',
340
      inputSchema: z.object({ conflictId: z.string() }),
341
      annotations: READ_ONLY_TOOL,
342
    },
343
    async (input, context) => {
344
      const binding = await requireBoundAgent(context);
345
      const engine = await resolveEngine(context);
346
      const conflict = engine.conflicts.get(input.conflictId);
347
      if (binding && (!conflict || !(binding.agent.agentId in conflict.permissions)))
348
        throw new Error('The bound agent is not a participant in this dispute');
349
      return text(engine.conflicts.get(input.conflictId)?.possibleResolutions ?? []);
350
    },
351
  );
352
  server.registerTool(
353
    OPENCLASP_TOOL_NAMES[9],
354
    {
355
      title: 'Complete interaction',
356
      description: 'Submit a signed completion receipt',
357
      inputSchema: ReceiptSchema,
358
      annotations: { ...WRITE_TOOL, idempotentHint: true },
359
    },
360
    async (input, context) => {
361
      const binding = await requireBoundAgent(context);
362
      if (binding && !input.participants.includes(binding.agent.agentId))
363
        throw new Error('Receipt participants must include the bound agent');
364
      const engine = await resolveEngine(context);
365
      const receipt = engine.submitReceipt(input);
366
      await persist(context, 'receipt', receipt.receiptId, receipt);
367
      return text(receipt);
368
    },
369
  );
370
  server.registerTool(
371
    OPENCLASP_TOOL_NAMES[10],
372
    {
373
      title: 'Submit bilateral feedback',
374
      description: 'Submit receipt-backed structured feedback',
375
      inputSchema: FeedbackSchema,
376
      annotations: { ...WRITE_TOOL, idempotentHint: true },
377
    },
378
    async (input, context) => {
379
      await requireBoundAgent(context, input.reviewerAgentId);
380
      const engine = await resolveEngine(context);
381
      const result = engine.submitFeedback(input);
382
      await persist(context, 'feedback', input.feedbackId, input);
383
      for (const profile of engine.profiles.values())
384
        await persist(
385
          context,
386
          'profile',
387
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
388
          profile,
389
        );
390
      return text(result);
391
    },
392
  );
393
  server.registerTool(
394
    OPENCLASP_TOOL_NAMES[11],
395
    {
396
      title: 'Raise dispute',
397
      description: 'Create a mutually consented dispute',
398
      inputSchema: z.object({
399
        interactionId: z.string().uuid(),
400
        issue: z.string(),
401
        participants: z.array(z.string()).min(2),
402
      }),
403
      annotations: WRITE_TOOL,
404
    },
405
    async (input, context) => {
406
      const binding = await requireBoundAgent(context);
407
      if (binding && !input.participants.includes(binding.agent.agentId))
408
        throw new Error('Dispute participants must include the bound agent');
409
      const engine = await resolveEngine(context);
410
      const conflict = engine.createConflict({
411
        interactionId: input.interactionId,
412
        issue: input.issue,
413
        participants: input.participants,
414
        positions: {},
415
        evidence: [],
416
        contractClauses: [],
417
        missingInformation: [],
418
        possibleResolutions: [],
419
      });
420
      await persist(context, 'conflict', conflict.conflictId, conflict);
421
      return text(conflict);
422
    },
423
  );
424
  server.registerTool(
425
    OPENCLASP_TOOL_NAMES[12],
426
    {
427
      title: 'Verify receipt',
428
      description: 'Verify a signed interaction receipt',
429
      inputSchema: ReceiptSchema,
430
      annotations: READ_ONLY_TOOL,
431
    },
432
    async (input, context) => {
433
      await requireBoundAgent(context);
434
      const engine = await resolveEngine(context);
435
      try {
436
        engine.verifyReceipt(input);
437
        return text({ valid: true });
438
      } catch {
439
        return text({ valid: false });
440
      }
441
    },
442
  );
443
  server.registerTool(
444
    OPENCLASP_TOOL_NAMES[13],
445
    {
446
      title: 'Set up this agent',
447
      description:
448
        'Propose this MCP installation as a new OpenClasp agent. The owner must confirm it in the dashboard before the identity is bound.',
449
      inputSchema: z
450
        .object({
451
          agentName: z.string().trim().min(1).max(100),
452
          projectName: z.string().trim().min(1).max(100).optional(),
453
          projectId: z.string().optional(),
454
          framework: z.string().trim().max(100).optional(),
455
          description: z.string().trim().max(500).optional(),
456
          agentVersion: z.string().trim().min(1).max(100).optional(),
457
          a2aEndpoint: z.string().url().optional(),
458
          capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
459
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
460
        })
461
        .refine((input) => input.projectName || input.projectId, {
462
          message: 'projectName or projectId is required',
463
        }),
464
      annotations: WRITE_TOOL,
465
    },
466
    async (input, context) => {
467
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
468
      const connection = installationContext(context);
469
      const current = await resolveInstallation(
470
        onboardingStore,
471
        connection.operatorId,
472
        connection.clientId,
473
      );
474
      if (current.status === 'connected') return text(current);
475
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
476
        clientId: connection.clientId,
477
        ...input,
478
      });
479
      return text({
480
        status: 'pending_confirmation',
481
        confirmationRequired: true,
482
        request,
483
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
484
        next: 'Ask the owner to confirm this setup request in the OpenClasp dashboard, then call openclasp_get_identity.',
485
      });
486
    },
487
  );
488
  server.registerTool(
489
    OPENCLASP_TOOL_NAMES[14],
490
    {
491
      title: 'Get my agent identity',
492
      description:
493
        'Return the agent identity automatically bound to this authenticated MCP installation.',
494
      inputSchema: z.object({}),
495
      annotations: READ_ONLY_TOOL,
496
    },
497
    async (_input, context) => {
498
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
499
      const connection = installationContext(context);
500
      return text(
501
        await resolveInstallation(onboardingStore, connection.operatorId, connection.clientId),
502
      );
503
    },
504
  );
505
  server.registerTool(
506
    OPENCLASP_TOOL_NAMES[15],
507
    {
508
      title: 'Switch agent identity',
509
      description:
510
        'Request that this MCP installation switch to another existing agent. Dashboard confirmation is required.',
511
      inputSchema: z.object({ existingAgentId: z.string().min(1) }),
512
      annotations: WRITE_TOOL,
513
    },
514
    async (input, context) => {
515
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
516
      const connection = installationContext(context);
517
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
518
        clientId: connection.clientId,
519
        action: 'switch',
520
        existingAgentId: input.existingAgentId,
521
      });
522
      return text({
523
        status: 'pending_confirmation',
524
        confirmationRequired: true,
525
        request,
526
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
527
      });
528
    },
529
  );
530
  server.registerTool(
531
    OPENCLASP_TOOL_NAMES[16],
532
    {
533
      title: 'Update my agent profile',
534
      description: 'Update the profile of the agent bound to this MCP installation.',
535
      inputSchema: z.object({
536
        name: z.string().trim().min(1).max(100).optional(),
537
        framework: z.string().trim().min(1).max(100).optional(),
538
        description: z.string().trim().max(500).optional(),
539
        agentVersion: z.string().trim().min(1).max(100).optional(),
540
        a2aEndpoint: z.union([z.string().url(), z.literal('')]).optional(),
541
        capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
542
        limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
543
      }),
544
      annotations: WRITE_TOOL,
545
    },
546
    async (input, context) => {
547
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
548
      const connection = installationContext(context);
549
      const agent = await updateAgentProfile(
550
        onboardingStore,
551
        connection.operatorId,
552
        connection.clientId,
553
        input,
554
      );
555
      const published = await agentDirectory?.getPublishedAgent(agent.agentId);
556
      if (published && agentDirectory?.publishAgent)
557
        await agentDirectory.publishAgent(
558
          connection.operatorId,
559
          buildPublicAgentCard(
560
            agent,
561
            process.env.OPENCLASP_PUBLIC_URL ??
562
              process.env.OPENCLASP_DASHBOARD_URL ??
563
              'https://openclasp.vercel.app',
564
            published,
565
          ),
566
        );
567
      return text(agent);
568
    },
569
  );
570
  server.registerTool(
571
    OPENCLASP_TOOL_NAMES[17],
572
    {
573
      title: 'Check OpenClasp connection',
574
      description:
575
        'Check whether this MCP installation is unbound, awaiting confirmation, or connected.',
576
      inputSchema: z.object({}),
577
      annotations: READ_ONLY_TOOL,
578
    },
579
    async (_input, context) => {
580
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
581
      const connection = installationContext(context);
582
      const binding = await resolveInstallation(
583
        onboardingStore,
584
        connection.operatorId,
585
        connection.clientId,
586
      );
587
      if (binding.status === 'connected') return text(binding);
588
      const state = await onboardingStore.list(connection.operatorId);
589
      const pending = state
590
        .filter((row) => row.kind === 'setup_request')
591
        .map((row) => row.payload as { clientId?: string; status?: string })
592
        .find(
593
          (request) => request.clientId === connection.clientId && request.status === 'pending',
594
        );
595
      return text(
596
        pending
597
          ? {
598
              status: 'pending_confirmation',
599
              confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
600
            }
601
          : binding,
602
      );
603
    },
604
  );
605
  server.registerTool(
606
    OPENCLASP_TOOL_NAMES[18],
607
    {
608
      title: 'Register delegation',
609
      description: 'Register and verify a signed, scoped delegation between two known agents.',
610
      inputSchema: DelegationCredentialSchema,
611
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
612
    },
613
    async (input, context) => {
614
      const binding = await requireBoundAgent(context);
615
      if (
616
        binding &&
617
        input.parentAgentId !== binding.agent.agentId &&
618
        input.childAgentId !== binding.agent.agentId
619
      )
620
        throw new Error('The bound agent is not a party to this delegation');
621
      const engine = await resolveEngine(context);
622
      engine.delegations.set(input.delegationId, input);
623
      if (!engine.verifyDelegation(input.delegationId)) {
624
        engine.delegations.delete(input.delegationId);
625
        throw new Error('Delegation verification failed');
626
      }
627
      await persist(context, 'delegation', input.delegationId, input);
628
      return text({ valid: true, delegation: input });
629
    },
630
  );
631
  server.registerTool(
632
    OPENCLASP_TOOL_NAMES[19],
633
    {
634
      title: 'Save signed agreement',
635
      description:
636
        'Verify and save a complete interaction contract signed by every participating agent.',
637
      inputSchema: InteractionContractSchema,
638
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
639
    },
640
    async (input, context) => {
641
      const binding = await requireBoundAgent(context);
642
      if (binding && !input.parties.includes(binding.agent.agentId))
643
        throw new Error('Contract parties must include the bound agent');
644
      const contract = (await resolveEngine(context)).saveContract(input);
645
      await persist(context, 'contract', contract.interactionId, contract);
646
      return text(contract);
647
    },
648
  );
649
  server.registerTool(
650
    OPENCLASP_TOOL_NAMES[20],
651
    {
652
      title: 'Consent to mediation',
653
      description:
654
        "Record the bound agent's explicit consent to mediation for a dispute it participates in.",
655
      inputSchema: z.object({ conflictId: z.string(), agentId: z.string().optional() }),
656
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
657
    },
658
    async (input, context) => {
659
      const binding = await requireBoundAgent(context);
660
      const agentId = binding?.agent.agentId ?? input.agentId;
661
      if (!agentId) throw new Error('agentId is required for local MCP');
662
      const engine = await resolveEngine(context);
663
      const conflict = engine.permitMediation(input.conflictId, agentId);
664
      await persist(context, 'conflict', conflict.conflictId, conflict);
665
      return text(conflict);
666
    },
667
  );
668
  server.registerTool(
669
    OPENCLASP_TOOL_NAMES[21],
670
    {
671
      title: 'Resolve mediated dispute',
672
      description:
673
        'Save an agreed resolution only after every dispute participant has consented to mediation.',
674
      inputSchema: z.object({ conflictId: z.string(), resolution: z.string().trim().min(1) }),
675
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
676
    },
677
    async (input, context) => {
678
      const binding = await requireBoundAgent(context);
679
      const engine = await resolveEngine(context);
680
      const existing = engine.conflicts.get(input.conflictId);
681
      if (binding && (!existing || !(binding.agent.agentId in existing.permissions)))
682
        throw new Error('The bound agent is not a participant in this dispute');
683
      const conflict = engine.resolveConflict(input.conflictId, input.resolution);
684
      await persist(context, 'conflict', conflict.conflictId, conflict);
685
      return text(conflict);
686
    },
687
  );
688
  server.registerTool(
689
    OPENCLASP_TOOL_NAMES[22],
690
    {
691
      title: 'Find published agent',
692
      description:
693
        'Find an exact owner-published agent card. Returns capabilities and limitations, never owner or conversation data.',
694
      inputSchema: z.object({ agentId: z.string().min(1) }),
695
      annotations: READ_ONLY_TOOL,
696
    },
697
    async (input, context) => {
698
      await requireBoundAgent(context);
699
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
700
      return text((await agentDirectory.getPublishedAgent(input.agentId)) ?? { found: false });
701
    },
702
  );
703
  server.registerTool(
704
    OPENCLASP_TOOL_NAMES[23],
705
    {
706
      title: 'Search published agents',
707
      description:
708
        'Search owner-published agent cards by name, framework, or capability without exposing owner identities or projects.',
709
      inputSchema: z.object({
710
        query: z.string().trim().max(100).optional(),
711
        capability: z.string().trim().max(100).optional(),
712
        limit: z.number().int().min(1).max(50).optional(),
713
      }),
714
      annotations: READ_ONLY_TOOL,
715
    },
716
    async (input, context) => {
717
      await requireBoundAgent(context);
718
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
719
      return text(await agentDirectory.searchPublishedAgents(input));
720
    },
721
  );
722
  server.registerTool(
723
    OPENCLASP_TOOL_NAMES[24],
724
    {
725
      title: 'Connect to another agent',
726
      description:
727
        'Create one immutable cross-account assurance contract and return the responder A2A endpoint and OpenClasp extension metadata.',
728
      inputSchema: z
729
        .object({
730
          targetAgentId: z.string().min(1).optional(),
731
          targetAgentCardUrl: z.string().url().optional(),
732
          purpose: z.string().trim().min(1).max(500),
733
          taskCategory: z.string().trim().min(1).max(100).default('general'),
734
          requestedOutcome: z.string().trim().min(1).max(1000),
735
          successCriteria: z.array(z.string().trim().min(1)).min(1).max(50),
736
          allowedActions: z.array(z.string().trim().min(1)).max(100).default([]),
737
          prohibitedActions: z.array(z.string().trim().min(1)).max(100).default([]),
738
          allowedData: z.array(z.string().trim().min(1)).max(100).default([]),
739
          prohibitedData: z.array(z.string().trim().min(1)).max(100).default([]),
740
          evidenceRequirements: z.array(z.string().trim().min(1)).max(100).default([]),
741
          deadline: z.string().datetime().optional(),
742
          expiresInMinutes: z.number().int().min(5).max(10080).default(60),
743
        })
744
        .refine((value) => value.targetAgentId || value.targetAgentCardUrl, {
745
          message: 'targetAgentId or targetAgentCardUrl is required',
746
        }),
747
      annotations: { ...WRITE_TOOL, openWorldHint: true },
748
    },
749
    async (input, context) => {
750
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
751
      const binding = await requireBoundAgent(context);
752
      if (!binding) throw new Error('A hosted, bound MCP installation is required');
753
      const connection = installationContext(context);
754
      const initiatorCard = await agentDirectory.getPublishedAgent(binding.agent.agentId);
755
      if (!initiatorCard)
756
        throw new Error('Publish this agent in the OpenClasp dashboard before connecting');
757
      let targetAgentId = input.targetAgentId;
758
      if (!targetAgentId && input.targetAgentCardUrl) {
759
        const cardUrl = new URL(input.targetAgentCardUrl);
760
        const trustedOrigin = new URL(
761
          process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app',
762
        ).origin;
763
        const match = cardUrl.pathname.match(/^\/agents\/([^/]+)\/card\.json$/);
764
        if (cardUrl.origin !== trustedOrigin || !match?.[1])
765
          throw new Error('Only OpenClasp-hosted Agent Card URLs are accepted');
766
        targetAgentId = decodeURIComponent(match[1]);
767
      }
768
      if (!targetAgentId) throw new Error('Target agent is required');
769
      const responderCard = await agentDirectory.getPublishedAgent(targetAgentId);
770
      if (!responderCard) throw new Error('Target agent is not published on OpenClasp');
771
      const responderTransport = responderCard.transports[0];
772
      if (!responderTransport) throw new Error('Target agent has not published an A2A endpoint');
773
      const now = new Date();
774
      const interactionId = crypto.randomUUID();
775
      const contract = {
776
        protocolVersion: '0.1' as const,
777
        interactionId,
778
        purpose: input.purpose,
779
        parties: [binding.agent.agentId, responderCard.agentId],
780
        taskCategory: input.taskCategory,
781
        requestedOutcome: input.requestedOutcome,
782
        successCriteria: input.successCriteria,
783
        allowedActions: input.allowedActions,
784
        prohibitedActions: input.prohibitedActions,
785
        allowedData: input.allowedData,
786
        prohibitedData: input.prohibitedData,
787
        ...(input.deadline ? { deadline: input.deadline } : {}),
788
        evidenceRequirements: input.evidenceRequirements,
789
        delegationRules: ['explicit_contract_scope'],
790
        humanApprovalRequirements: [],
791
        factCheckingPolicy: 'important_claims',
792
        mediationPolicy: 'mutual_consent' as const,
793
        retentionDays: 30,
794
        completionConditions: input.successCriteria,
795
        cancellationConditions: ['either_party_before_completion'],
796
        signatures: {},
797
      };
798
      const termsHash = canonicalHash(contract);
799
      const createdAt = now.toISOString();
800
      const interaction: FederatedInteraction = {
801
        protocolVersion: '0.1',
802
        interactionId,
803
        initiatorAgentId: binding.agent.agentId,
804
        responderAgentId: responderCard.agentId,
805
        status: 'pending',
806
        contract,
807
        termsHash,
808
        acceptances: {
809
          [binding.agent.agentId]: {
810
            agentId: binding.agent.agentId,
811
            method: 'oauth_installation',
812
            termsHash,
813
            acceptedAt: createdAt,
814
          },
815
        },
816
        ...(initiatorCard.transports[0] ? { initiatorTransport: initiatorCard.transports[0] } : {}),
817
        responderTransport,
818
        createdAt,
819
        updatedAt: createdAt,
820
        expiresAt: new Date(now.getTime() + input.expiresInMinutes * 60_000).toISOString(),
821
      };
822
      const stored = await agentDirectory.createFederatedInteraction(
823
        connection.operatorId,
824
        interaction,
825
      );
826
      return text({
827
        interaction: stored,
828
        a2a: {
829
          endpoint: responderTransport.endpoint,
830
          protocolBinding: responderTransport.protocolBinding,
831
          extensions: [DEFAULT_EXTENSION_URI],
832
          metadata: {
833
            [DEFAULT_EXTENSION_URI]: {
834
              interactionId,
835
              termsHash,
836
              initiatorAgentId: binding.agent.agentId,
837
              responderAgentId: responderCard.agentId,
838
            },
839
          },
840
        },
841
        next: 'Wait for the responder to accept. Once active, send messages directly to its A2A endpoint with this extension metadata.',
842
      });
843
    },
844
  );
845
  server.registerTool(
846
    OPENCLASP_TOOL_NAMES[25],
847
    {
848
      title: 'List shared invitations',
849
      description: 'List incoming and outgoing cross-account interaction invitations and status.',
850
      inputSchema: z.object({}),
851
      annotations: READ_ONLY_TOOL,
852
    },
853
    async (_input, context) => {
854
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
855
      await requireBoundAgent(context);
856
      const connection = installationContext(context);
857
      return text(await agentDirectory.listFederatedInteractions(connection.operatorId));
858
    },
859
  );
860
  server.registerTool(
861
    OPENCLASP_TOOL_NAMES[26],
862
    {
863
      title: 'Respond to agent invitation',
864
      description: 'Accept or reject an invitation as the agent bound to this MCP installation.',
865
      inputSchema: z.object({
866
        interactionId: z.string().uuid(),
867
        decision: z.enum(['accept', 'reject']),
868
      }),
869
      annotations: WRITE_TOOL,
870
    },
871
    async (input, context) => {
872
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
873
      const binding = await requireBoundAgent(context);
874
      if (!binding) throw new Error('A hosted, bound MCP installation is required');
875
      const connection = installationContext(context);
876
      return text(
877
        await agentDirectory.respondToFederatedInteraction(
878
          connection.operatorId,
879
          input.interactionId,
880
          binding.agent.agentId,
881
          input.decision,
882
          'oauth_installation',
883
        ),
884
      );
885
    },
886
  );
887
  server.registerTool(
888
    OPENCLASP_TOOL_NAMES[27],
889
    {
890
      title: 'Get shared interaction',
891
      description: 'Get the canonical contract and bilateral acceptance state for one interaction.',
892
      inputSchema: z.object({ interactionId: z.string().uuid() }),
893
      annotations: READ_ONLY_TOOL,
894
    },
895
    async (input, context) => {
896
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
897
      await requireBoundAgent(context);
898
      const connection = installationContext(context);
899
      const value = await agentDirectory.getFederatedInteraction(
900
        connection.operatorId,
901
        input.interactionId,
902
      );
903
      if (!value) throw new Error('Interaction not found');
904
      return text(value);
905
    },
906
  );
907
  return server;
908
}