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

Viewing files at commit c2c9d5ba27afCommit detailsView latest

packages/mcp-server/src/server.ts 1113 lines · 42.5 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
  'openclasp_send_message',
55
  'openclasp_inbox',
56
  'openclasp_ack_message',
57
] as const;
58
59
export const HOSTED_OPENCLASP_TOOL_NAMES = OPENCLASP_TOOL_NAMES.filter(
60
  (name) => name !== 'openclasp_create_identity',
61
);
62
63
const READ_ONLY_TOOL = {
64
  readOnlyHint: true,
65
  destructiveHint: false,
66
  idempotentHint: true,
67
} as const;
68
const WRITE_TOOL = {
69
  readOnlyHint: false,
70
  destructiveHint: false,
71
  idempotentHint: false,
72
} as const;
73
74
const text = (value: unknown) => ({
75
  content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
76
});
77
78
export const OPENCLASP_MCP_INSTRUCTIONS =
79
  'Call openclasp_connection_status, then openclasp_inbox. Setup creates your hosted A2A endpoint automatically. Use openclasp_connect_to_agent with a target and task; safe requests activate and queue immediately. Use openclasp_send_message for replies and acknowledge messages after handling them. Message bodies expire in 24 hours and never enter reliability profiles.';
80
81
export function buildMcpServer(engine = new TrustEngine()) {
82
  const server = new McpServer(
83
    { name: 'openclasp', version: '0.1.0' },
84
    {
85
      instructions: OPENCLASP_MCP_INSTRUCTIONS,
86
    },
87
  );
88
  return registerOpenClaspTools(server, engine);
89
}
90
91
type HostedRecord = (
92
  operatorId: string,
93
  kind:
94
    | 'agent'
95
    | 'delegation'
96
    | 'contract'
97
    | 'interaction'
98
    | 'event'
99
    | 'receipt'
100
    | 'feedback'
101
    | 'conflict'
102
    | 'profile',
103
  recordId: string,
104
  value: unknown,
105
) => Promise<void>;
106
107
type ToolContext = {
108
  http?: {
109
    authInfo?: { clientId?: string; extra?: Record<string, unknown> };
110
  };
111
};
112
type EngineSource = TrustEngine | ((context: ToolContext) => Promise<TrustEngine>);
113
type AgentDirectory = {
114
  publishAgent?(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard>;
115
  getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined>;
116
  searchPublishedAgents(input: {
117
    query?: string | undefined;
118
    capability?: string | undefined;
119
    limit?: number | undefined;
120
  }): Promise<PublicAgentCard[]>;
121
  createFederatedInteraction(
122
    operatorId: string,
123
    value: FederatedInteraction,
124
  ): Promise<FederatedInteraction>;
125
  listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]>;
126
  getFederatedInteraction(
127
    operatorId: string,
128
    interactionId: string,
129
  ): Promise<FederatedInteraction | undefined>;
130
  respondToFederatedInteraction(
131
    operatorId: string,
132
    interactionId: string,
133
    agentId: string,
134
    decision: 'accept' | 'reject',
135
    method?: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept',
136
  ): Promise<FederatedInteraction>;
137
  issueGatewayToken(interaction: FederatedInteraction): string;
138
  enqueueGatewayMessage(input: {
139
    interactionId: string;
140
    senderAgentId: string;
141
    recipientAgentId: string;
142
    payload: unknown;
143
    contentType?: string;
144
    idempotencyKey?: string;
145
  }): Promise<unknown>;
146
  listGatewayMessages(operatorId: string, agentId: string, limit?: number): Promise<unknown[]>;
147
  acknowledgeGatewayMessage(
148
    operatorId: string,
149
    agentId: string,
150
    messageId: string,
151
  ): Promise<unknown>;
152
};
153
154
function installationContext(context: ToolContext) {
155
  const operatorId = context.http?.authInfo?.extra?.operatorId;
156
  const clientId = context.http?.authInfo?.clientId;
157
  if (typeof operatorId !== 'string' || typeof clientId !== 'string')
158
    throw new Error('Authenticated MCP installation context is required');
159
  return { operatorId, clientId };
160
}
161
162
export function registerOpenClaspTools(
163
  server: McpServer,
164
  engineSource: EngineSource = new TrustEngine(),
165
  recordHosted?: HostedRecord,
166
  onboardingStore?: OnboardingStore,
167
  agentDirectory?: AgentDirectory,
168
) {
169
  const factChecker = new FixtureFactCheckProvider();
170
  const resolveEngine = (context: ToolContext) =>
171
    typeof engineSource === 'function' ? engineSource(context) : Promise.resolve(engineSource);
172
  const persist = async (
173
    context: ToolContext,
174
    kind: Parameters<HostedRecord>[1],
175
    id: string,
176
    value: unknown,
177
  ) => {
178
    const operatorId = context.http?.authInfo?.extra?.operatorId;
179
    if (recordHosted && typeof operatorId === 'string')
180
      await recordHosted(operatorId, kind, id, value);
181
  };
182
183
  const requireBoundAgent = async (context: ToolContext, claimedAgentId?: string) => {
184
    if (!onboardingStore) return undefined;
185
    const connection = installationContext(context);
186
    const binding = await resolveInstallation(
187
      onboardingStore,
188
      connection.operatorId,
189
      connection.clientId,
190
    );
191
    if (binding.status !== 'connected')
192
      throw new Error('Call openclasp_setup and obtain owner confirmation before using this tool');
193
    if (claimedAgentId && binding.agent.agentId !== claimedAgentId)
194
      throw new Error('The claimed agent does not match this MCP installation');
195
    return binding;
196
  };
197
198
  if (!onboardingStore)
199
    server.registerTool(
200
      OPENCLASP_TOOL_NAMES[0],
201
      {
202
        title: 'Create local cryptographic identity',
203
        description:
204
          'Create an Ed25519 identity for local development. This returns private key material and is intentionally unavailable on hosted MCP.',
205
        inputSchema: z.object({
206
          agentId: z.string(),
207
          operatorRef: z.string(),
208
          capabilities: z.array(z.string()),
209
        }),
210
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
211
      },
212
      async (input, context) => {
213
        const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
214
        const created = createIdentity({
215
          ...input,
216
          operatorRef:
217
            typeof authenticatedOperator === 'string'
218
              ? `auth0:${authenticatedOperator}`
219
              : input.operatorRef,
220
        });
221
        return text(created);
222
      },
223
    );
224
  server.registerTool(
225
    OPENCLASP_TOOL_NAMES[1],
226
    {
227
      title: 'Register cryptographic identity',
228
      description: 'Register and verify a signed agent identity',
229
      inputSchema: AgentIdentitySchema,
230
      annotations: { ...WRITE_TOOL, idempotentHint: true },
231
    },
232
    async (input, context) => {
233
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
234
      if (
235
        typeof authenticatedOperator === 'string' &&
236
        input.operatorRef !== `auth0:${authenticatedOperator}`
237
      ) {
238
        throw new Error('Agent identity is not owned by the authenticated operator');
239
      }
240
      await requireBoundAgent(context, input.agentId);
241
      const engine = await resolveEngine(context);
242
      const registered = engine.registerAgent(input);
243
      await persist(context, 'agent', registered.agentId, registered);
244
      return text(registered);
245
    },
246
  );
247
  server.registerTool(
248
    OPENCLASP_TOOL_NAMES[2],
249
    {
250
      title: 'Get contextual reliability',
251
      description: 'Get a task-specific behavioural profile',
252
      inputSchema: z.object({ agentId: z.string(), version: z.string(), taskCategory: z.string() }),
253
      annotations: READ_ONLY_TOOL,
254
    },
255
    async (input, context) => {
256
      await requireBoundAgent(context);
257
      const engine = await resolveEngine(context);
258
      return text(engine.getRisk(input.agentId, input.version, input.taskCategory));
259
    },
260
  );
261
  server.registerTool(
262
    OPENCLASP_TOOL_NAMES[3],
263
    {
264
      title: 'Assess counterparty',
265
      description: 'Assess a counterparty in context',
266
      inputSchema: z.object({
267
        envelope: TrustEnvelopeSchema,
268
        action: z.string(),
269
        dataClasses: z.array(z.string()).optional(),
270
        humanApproved: z.boolean().optional(),
271
      }),
272
      annotations: READ_ONLY_TOOL,
273
    },
274
    async (input, context) => {
275
      await requireBoundAgent(context);
276
      const engine = await resolveEngine(context);
277
      return text(engine.assess(input as any));
278
    },
279
  );
280
  server.registerTool(
281
    OPENCLASP_TOOL_NAMES[4],
282
    {
283
      title: 'Begin assured interaction',
284
      description: 'Begin an interaction and return its identifier',
285
      inputSchema: z.object({
286
        purpose: z.string(),
287
        parties: z.array(z.string()),
288
        taskCategory: z.string(),
289
      }),
290
      annotations: WRITE_TOOL,
291
    },
292
    async (input, context) => {
293
      const binding = await requireBoundAgent(context);
294
      if (binding && !input.parties.includes(binding.agent.agentId))
295
        throw new Error('Interaction parties must include the agent bound to this installation');
296
      const interaction = {
297
        interactionId: crypto.randomUUID(),
298
        status: 'contract_required',
299
        createdAt: new Date().toISOString(),
300
        ...input,
301
      };
302
      await persist(context, 'interaction', interaction.interactionId, interaction);
303
      return text(interaction);
304
    },
305
  );
306
  server.registerTool(
307
    OPENCLASP_TOOL_NAMES[5],
308
    {
309
      title: 'Record signed event',
310
      description: 'Validate and record a signed structured event',
311
      inputSchema: InteractionEventSchema,
312
      annotations: { ...WRITE_TOOL, idempotentHint: true },
313
    },
314
    async (input, context) => {
315
      await requireBoundAgent(context, input.agentId);
316
      const engine = await resolveEngine(context);
317
      const event = engine.recordEvent(input);
318
      await persist(context, 'event', event.eventId, event);
319
      return text(event);
320
    },
321
  );
322
  server.registerTool(
323
    OPENCLASP_TOOL_NAMES[6],
324
    {
325
      title: 'Check factual claim',
326
      description: 'Check an objective factual claim',
327
      inputSchema: z.object({ claim: z.string(), permission: z.boolean().optional() }),
328
      annotations: { ...READ_ONLY_TOOL, openWorldHint: true },
329
    },
330
    async (input) => text(await factChecker.check(input.claim, input.permission)),
331
  );
332
  server.registerTool(
333
    OPENCLASP_TOOL_NAMES[7],
334
    {
335
      title: 'Validate commitment',
336
      description: 'Check whether a commitment is represented by a signed event',
337
      inputSchema: z.object({ interactionId: z.string().uuid(), commitment: z.string() }),
338
      annotations: READ_ONLY_TOOL,
339
    },
340
    async (input, context) => {
341
      await requireBoundAgent(context);
342
      const engine = await resolveEngine(context);
343
      return text({
344
        valid: [...engine.events.values()].some(
345
          (event) =>
346
            event.interactionId === input.interactionId &&
347
            event.eventType === 'commitment' &&
348
            JSON.stringify(event.payload).includes(input.commitment),
349
        ),
350
      });
351
    },
352
  );
353
  server.registerTool(
354
    OPENCLASP_TOOL_NAMES[8],
355
    {
356
      title: 'Suggest dispute resolution',
357
      description: 'Suggest attributable resolutions for a consented conflict',
358
      inputSchema: z.object({ conflictId: z.string() }),
359
      annotations: READ_ONLY_TOOL,
360
    },
361
    async (input, context) => {
362
      const binding = await requireBoundAgent(context);
363
      const engine = await resolveEngine(context);
364
      const conflict = engine.conflicts.get(input.conflictId);
365
      if (binding && (!conflict || !(binding.agent.agentId in conflict.permissions)))
366
        throw new Error('The bound agent is not a participant in this dispute');
367
      return text(engine.conflicts.get(input.conflictId)?.possibleResolutions ?? []);
368
    },
369
  );
370
  server.registerTool(
371
    OPENCLASP_TOOL_NAMES[9],
372
    {
373
      title: 'Complete interaction',
374
      description: 'Submit a signed completion receipt',
375
      inputSchema: ReceiptSchema,
376
      annotations: { ...WRITE_TOOL, idempotentHint: true },
377
    },
378
    async (input, context) => {
379
      const binding = await requireBoundAgent(context);
380
      if (binding && !input.participants.includes(binding.agent.agentId))
381
        throw new Error('Receipt participants must include the bound agent');
382
      const engine = await resolveEngine(context);
383
      const receipt = engine.submitReceipt(input);
384
      await persist(context, 'receipt', receipt.receiptId, receipt);
385
      return text(receipt);
386
    },
387
  );
388
  server.registerTool(
389
    OPENCLASP_TOOL_NAMES[10],
390
    {
391
      title: 'Submit bilateral feedback',
392
      description: 'Submit receipt-backed structured feedback',
393
      inputSchema: FeedbackSchema,
394
      annotations: { ...WRITE_TOOL, idempotentHint: true },
395
    },
396
    async (input, context) => {
397
      await requireBoundAgent(context, input.reviewerAgentId);
398
      const engine = await resolveEngine(context);
399
      const result = engine.submitFeedback(input);
400
      await persist(context, 'feedback', input.feedbackId, input);
401
      for (const profile of engine.profiles.values())
402
        await persist(
403
          context,
404
          'profile',
405
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
406
          profile,
407
        );
408
      return text(result);
409
    },
410
  );
411
  server.registerTool(
412
    OPENCLASP_TOOL_NAMES[11],
413
    {
414
      title: 'Raise dispute',
415
      description: 'Create a mutually consented dispute',
416
      inputSchema: z.object({
417
        interactionId: z.string().uuid(),
418
        issue: z.string(),
419
        participants: z.array(z.string()).min(2),
420
      }),
421
      annotations: WRITE_TOOL,
422
    },
423
    async (input, context) => {
424
      const binding = await requireBoundAgent(context);
425
      if (binding && !input.participants.includes(binding.agent.agentId))
426
        throw new Error('Dispute participants must include the bound agent');
427
      const engine = await resolveEngine(context);
428
      const conflict = engine.createConflict({
429
        interactionId: input.interactionId,
430
        issue: input.issue,
431
        participants: input.participants,
432
        positions: {},
433
        evidence: [],
434
        contractClauses: [],
435
        missingInformation: [],
436
        possibleResolutions: [],
437
      });
438
      await persist(context, 'conflict', conflict.conflictId, conflict);
439
      return text(conflict);
440
    },
441
  );
442
  server.registerTool(
443
    OPENCLASP_TOOL_NAMES[12],
444
    {
445
      title: 'Verify receipt',
446
      description: 'Verify a signed interaction receipt',
447
      inputSchema: ReceiptSchema,
448
      annotations: READ_ONLY_TOOL,
449
    },
450
    async (input, context) => {
451
      await requireBoundAgent(context);
452
      const engine = await resolveEngine(context);
453
      try {
454
        engine.verifyReceipt(input);
455
        return text({ valid: true });
456
      } catch {
457
        return text({ valid: false });
458
      }
459
    },
460
  );
461
  server.registerTool(
462
    OPENCLASP_TOOL_NAMES[13],
463
    {
464
      title: 'Set up this agent',
465
      description:
466
        'Propose this installation and safe automation policy. One owner approval binds it and creates its hosted A2A endpoint.',
467
      inputSchema: z
468
        .object({
469
          agentName: z.string().trim().min(1).max(100),
470
          projectName: z.string().trim().min(1).max(100).optional(),
471
          projectId: z.string().optional(),
472
          framework: z.string().trim().max(100).optional(),
473
          description: z.string().trim().max(500).optional(),
474
          agentVersion: z.string().trim().min(1).max(100).optional(),
475
          autoPublish: z.boolean().optional(),
476
          autoAcceptPolicy: z.enum(['off', 'safe_matching']).optional(),
477
          autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
478
          capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
479
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
480
        })
481
        .refine((input) => input.projectName || input.projectId, {
482
          message: 'projectName or projectId is required',
483
        }),
484
      annotations: WRITE_TOOL,
485
    },
486
    async (input, context) => {
487
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
488
      const connection = installationContext(context);
489
      const current = await resolveInstallation(
490
        onboardingStore,
491
        connection.operatorId,
492
        connection.clientId,
493
      );
494
      if (current.status === 'connected') return text(current);
495
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
496
        clientId: connection.clientId,
497
        ...input,
498
      });
499
      return text({
500
        status: 'pending_confirmation',
501
        confirmationRequired: true,
502
        request,
503
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
504
        next: 'Ask the owner to confirm this setup request in the OpenClasp dashboard, then call openclasp_get_identity.',
505
      });
506
    },
507
  );
508
  server.registerTool(
509
    OPENCLASP_TOOL_NAMES[14],
510
    {
511
      title: 'Get my agent identity',
512
      description:
513
        'Return the agent identity automatically bound to this authenticated MCP installation.',
514
      inputSchema: z.object({}),
515
      annotations: READ_ONLY_TOOL,
516
    },
517
    async (_input, context) => {
518
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
519
      const connection = installationContext(context);
520
      return text(
521
        await resolveInstallation(onboardingStore, connection.operatorId, connection.clientId),
522
      );
523
    },
524
  );
525
  server.registerTool(
526
    OPENCLASP_TOOL_NAMES[15],
527
    {
528
      title: 'Switch agent identity',
529
      description:
530
        'Request that this MCP installation switch to another existing agent. Dashboard confirmation is required.',
531
      inputSchema: z.object({ existingAgentId: z.string().min(1) }),
532
      annotations: WRITE_TOOL,
533
    },
534
    async (input, context) => {
535
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
536
      const connection = installationContext(context);
537
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
538
        clientId: connection.clientId,
539
        action: 'switch',
540
        existingAgentId: input.existingAgentId,
541
      });
542
      return text({
543
        status: 'pending_confirmation',
544
        confirmationRequired: true,
545
        request,
546
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
547
      });
548
    },
549
  );
550
  server.registerTool(
551
    OPENCLASP_TOOL_NAMES[16],
552
    {
553
      title: 'Update my agent profile',
554
      description: 'Update the profile of the agent bound to this MCP installation.',
555
      inputSchema: z.object({
556
        name: z.string().trim().min(1).max(100).optional(),
557
        framework: z.string().trim().min(1).max(100).optional(),
558
        description: z.string().trim().max(500).optional(),
559
        agentVersion: z.string().trim().min(1).max(100).optional(),
560
        autoPublish: z.boolean().optional(),
561
        autoAcceptPolicy: z.enum(['off', 'safe_matching']).optional(),
562
        autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
563
        capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
564
        limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
565
      }),
566
      annotations: WRITE_TOOL,
567
    },
568
    async (input, context) => {
569
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
570
      const connection = installationContext(context);
571
      const agent = await updateAgentProfile(
572
        onboardingStore,
573
        connection.operatorId,
574
        connection.clientId,
575
        input,
576
      );
577
      const published = await agentDirectory?.getPublishedAgent(agent.agentId);
578
      if (published && agentDirectory?.publishAgent)
579
        await agentDirectory.publishAgent(
580
          connection.operatorId,
581
          buildPublicAgentCard(
582
            agent,
583
            process.env.OPENCLASP_PUBLIC_URL ??
584
              process.env.OPENCLASP_DASHBOARD_URL ??
585
              'https://openclasp.vercel.app',
586
            published,
587
          ),
588
        );
589
      return text(agent);
590
    },
591
  );
592
  server.registerTool(
593
    OPENCLASP_TOOL_NAMES[17],
594
    {
595
      title: 'Check OpenClasp connection',
596
      description:
597
        'Check whether this MCP installation is unbound, awaiting confirmation, or connected.',
598
      inputSchema: z.object({}),
599
      annotations: READ_ONLY_TOOL,
600
    },
601
    async (_input, context) => {
602
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
603
      const connection = installationContext(context);
604
      const binding = await resolveInstallation(
605
        onboardingStore,
606
        connection.operatorId,
607
        connection.clientId,
608
      );
609
      if (binding.status === 'connected') return text(binding);
610
      const state = await onboardingStore.list(connection.operatorId);
611
      const pending = state
612
        .filter((row) => row.kind === 'setup_request')
613
        .map((row) => row.payload as { clientId?: string; status?: string })
614
        .find(
615
          (request) => request.clientId === connection.clientId && request.status === 'pending',
616
        );
617
      return text(
618
        pending
619
          ? {
620
              status: 'pending_confirmation',
621
              confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
622
            }
623
          : binding,
624
      );
625
    },
626
  );
627
  server.registerTool(
628
    OPENCLASP_TOOL_NAMES[18],
629
    {
630
      title: 'Register delegation',
631
      description: 'Register and verify a signed, scoped delegation between two known agents.',
632
      inputSchema: DelegationCredentialSchema,
633
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
634
    },
635
    async (input, context) => {
636
      const binding = await requireBoundAgent(context);
637
      if (
638
        binding &&
639
        input.parentAgentId !== binding.agent.agentId &&
640
        input.childAgentId !== binding.agent.agentId
641
      )
642
        throw new Error('The bound agent is not a party to this delegation');
643
      const engine = await resolveEngine(context);
644
      engine.delegations.set(input.delegationId, input);
645
      if (!engine.verifyDelegation(input.delegationId)) {
646
        engine.delegations.delete(input.delegationId);
647
        throw new Error('Delegation verification failed');
648
      }
649
      await persist(context, 'delegation', input.delegationId, input);
650
      return text({ valid: true, delegation: input });
651
    },
652
  );
653
  server.registerTool(
654
    OPENCLASP_TOOL_NAMES[19],
655
    {
656
      title: 'Save signed agreement',
657
      description:
658
        'Verify and save a complete interaction contract signed by every participating agent.',
659
      inputSchema: InteractionContractSchema,
660
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
661
    },
662
    async (input, context) => {
663
      const binding = await requireBoundAgent(context);
664
      if (binding && !input.parties.includes(binding.agent.agentId))
665
        throw new Error('Contract parties must include the bound agent');
666
      const contract = (await resolveEngine(context)).saveContract(input);
667
      await persist(context, 'contract', contract.interactionId, contract);
668
      return text(contract);
669
    },
670
  );
671
  server.registerTool(
672
    OPENCLASP_TOOL_NAMES[20],
673
    {
674
      title: 'Consent to mediation',
675
      description:
676
        "Record the bound agent's explicit consent to mediation for a dispute it participates in.",
677
      inputSchema: z.object({ conflictId: z.string(), agentId: z.string().optional() }),
678
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
679
    },
680
    async (input, context) => {
681
      const binding = await requireBoundAgent(context);
682
      const agentId = binding?.agent.agentId ?? input.agentId;
683
      if (!agentId) throw new Error('agentId is required for local MCP');
684
      const engine = await resolveEngine(context);
685
      const conflict = engine.permitMediation(input.conflictId, agentId);
686
      await persist(context, 'conflict', conflict.conflictId, conflict);
687
      return text(conflict);
688
    },
689
  );
690
  server.registerTool(
691
    OPENCLASP_TOOL_NAMES[21],
692
    {
693
      title: 'Resolve mediated dispute',
694
      description:
695
        'Save an agreed resolution only after every dispute participant has consented to mediation.',
696
      inputSchema: z.object({ conflictId: z.string(), resolution: z.string().trim().min(1) }),
697
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
698
    },
699
    async (input, context) => {
700
      const binding = await requireBoundAgent(context);
701
      const engine = await resolveEngine(context);
702
      const existing = engine.conflicts.get(input.conflictId);
703
      if (binding && (!existing || !(binding.agent.agentId in existing.permissions)))
704
        throw new Error('The bound agent is not a participant in this dispute');
705
      const conflict = engine.resolveConflict(input.conflictId, input.resolution);
706
      await persist(context, 'conflict', conflict.conflictId, conflict);
707
      return text(conflict);
708
    },
709
  );
710
  server.registerTool(
711
    OPENCLASP_TOOL_NAMES[22],
712
    {
713
      title: 'Find published agent',
714
      description:
715
        'Find an exact owner-published agent card. Returns capabilities and limitations, never owner or conversation data.',
716
      inputSchema: z.object({ agentId: z.string().min(1) }),
717
      annotations: READ_ONLY_TOOL,
718
    },
719
    async (input, context) => {
720
      await requireBoundAgent(context);
721
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
722
      return text((await agentDirectory.getPublishedAgent(input.agentId)) ?? { found: false });
723
    },
724
  );
725
  server.registerTool(
726
    OPENCLASP_TOOL_NAMES[23],
727
    {
728
      title: 'Search published agents',
729
      description:
730
        'Search owner-published agent cards by name, framework, or capability without exposing owner identities or projects.',
731
      inputSchema: z.object({
732
        query: z.string().trim().max(100).optional(),
733
        capability: z.string().trim().max(100).optional(),
734
        limit: z.number().int().min(1).max(50).optional(),
735
      }),
736
      annotations: READ_ONLY_TOOL,
737
    },
738
    async (input, context) => {
739
      await requireBoundAgent(context);
740
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
741
      return text(await agentDirectory.searchPublishedAgents(input));
742
    },
743
  );
744
  server.registerTool(
745
    OPENCLASP_TOOL_NAMES[24],
746
    {
747
      title: 'Automatically connect to another agent',
748
      description:
749
        'Infer safe contract defaults, auto-activate when the responder policy permits, and return a ready-to-send A2A request.',
750
      inputSchema: z
751
        .object({
752
          targetAgentId: z.string().min(1).optional(),
753
          targetAgentCardUrl: z.string().url().optional(),
754
          task: z.string().trim().min(1).max(2000).optional(),
755
          purpose: z.string().trim().min(1).max(500).optional(),
756
          taskCategory: z.string().trim().min(1).max(100).optional(),
757
          requestedOutcome: z.string().trim().min(1).max(1000).optional(),
758
          successCriteria: z.array(z.string().trim().min(1)).min(1).max(50).optional(),
759
          allowedActions: z.array(z.string().trim().min(1)).max(100).default([]),
760
          prohibitedActions: z.array(z.string().trim().min(1)).max(100).default([]),
761
          allowedData: z.array(z.string().trim().min(1)).max(100).default([]),
762
          prohibitedData: z.array(z.string().trim().min(1)).max(100).default([]),
763
          evidenceRequirements: z.array(z.string().trim().min(1)).max(100).default([]),
764
          deadline: z.string().datetime().optional(),
765
          expiresInMinutes: z.number().int().min(5).max(10080).default(60),
766
        })
767
        .superRefine((value, context) => {
768
          if (!value.targetAgentId && !value.targetAgentCardUrl)
769
            context.addIssue({
770
              code: 'custom',
771
              message: 'targetAgentId or targetAgentCardUrl is required',
772
            });
773
          if (!value.task && !value.purpose)
774
            context.addIssue({ code: 'custom', message: 'task or purpose is required' });
775
        }),
776
      annotations: { ...WRITE_TOOL, openWorldHint: true },
777
    },
778
    async (input, context) => {
779
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
780
      const binding = await requireBoundAgent(context);
781
      if (!binding) throw new Error('A hosted, bound MCP installation is required');
782
      const connection = installationContext(context);
783
      let initiatorCard = await agentDirectory.getPublishedAgent(binding.agent.agentId);
784
      if (!initiatorCard && binding.agent.autoPublish && agentDirectory.publishAgent)
785
        initiatorCard = await agentDirectory.publishAgent(
786
          connection.operatorId,
787
          buildPublicAgentCard(
788
            binding.agent,
789
            process.env.OPENCLASP_PUBLIC_URL ??
790
              process.env.OPENCLASP_DASHBOARD_URL ??
791
              'https://openclasp.vercel.app',
792
          ),
793
        );
794
      if (!initiatorCard)
795
        throw new Error(
796
          'This agent is private. Enable automatic publishing once in its dashboard settings.',
797
        );
798
      let targetAgentId = input.targetAgentId;
799
      if (!targetAgentId && input.targetAgentCardUrl) {
800
        const cardUrl = new URL(input.targetAgentCardUrl);
801
        const trustedOrigin = new URL(
802
          process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app',
803
        ).origin;
804
        const match = cardUrl.pathname.match(/^\/agents\/([^/]+)\/card\.json$/);
805
        if (cardUrl.origin !== trustedOrigin || !match?.[1])
806
          throw new Error('Only OpenClasp-hosted Agent Card URLs are accepted');
807
        targetAgentId = decodeURIComponent(match[1]);
808
      }
809
      if (!targetAgentId) throw new Error('Target agent is required');
810
      const responderCard = await agentDirectory.getPublishedAgent(targetAgentId);
811
      if (!responderCard) throw new Error('Target agent is not published on OpenClasp');
812
      const responderTransport = responderCard.transports[0];
813
      if (!responderTransport) throw new Error('Target agent has not published an A2A endpoint');
814
      const task = input.task ?? input.purpose!;
815
      const purpose = input.purpose ?? task.slice(0, 500);
816
      const taskCategory = input.taskCategory ?? responderCard.capabilities[0] ?? 'general';
817
      const requestedOutcome = input.requestedOutcome ?? task.slice(0, 1000);
818
      const successCriteria = input.successCriteria ?? [
819
        'Return a clear result that directly addresses the requested task',
820
      ];
821
      const existing = (await agentDirectory.listFederatedInteractions(connection.operatorId)).find(
822
        (candidate) =>
823
          candidate.initiatorAgentId === binding.agent.agentId &&
824
          candidate.responderAgentId === responderCard.agentId &&
825
          ['pending', 'active'].includes(candidate.status) &&
826
          candidate.contract.purpose === purpose &&
827
          candidate.contract.taskCategory === taskCategory &&
828
          candidate.contract.requestedOutcome === requestedOutcome &&
829
          JSON.stringify(candidate.contract.successCriteria) === JSON.stringify(successCriteria) &&
830
          JSON.stringify(candidate.contract.allowedActions) ===
831
            JSON.stringify(input.allowedActions) &&
832
          JSON.stringify(candidate.contract.prohibitedActions) ===
833
            JSON.stringify(input.prohibitedActions) &&
834
          JSON.stringify(candidate.contract.allowedData) === JSON.stringify(input.allowedData) &&
835
          JSON.stringify(candidate.contract.prohibitedData) ===
836
            JSON.stringify(input.prohibitedData) &&
837
          JSON.stringify(candidate.contract.evidenceRequirements) ===
838
            JSON.stringify(input.evidenceRequirements) &&
839
          candidate.contract.deadline === input.deadline,
840
      );
841
      const now = new Date();
842
      const interactionId = existing?.interactionId ?? crypto.randomUUID();
843
      const contract = {
844
        protocolVersion: '0.1' as const,
845
        interactionId,
846
        purpose,
847
        parties: [binding.agent.agentId, responderCard.agentId],
848
        taskCategory,
849
        requestedOutcome,
850
        successCriteria,
851
        allowedActions: input.allowedActions,
852
        prohibitedActions: input.prohibitedActions,
853
        allowedData: input.allowedData,
854
        prohibitedData: input.prohibitedData,
855
        ...(input.deadline ? { deadline: input.deadline } : {}),
856
        evidenceRequirements: input.evidenceRequirements,
857
        delegationRules: ['explicit_contract_scope'],
858
        humanApprovalRequirements: [],
859
        factCheckingPolicy: 'important_claims',
860
        mediationPolicy: 'mutual_consent' as const,
861
        retentionDays: 30,
862
        completionConditions: successCriteria,
863
        cancellationConditions: ['either_party_before_completion'],
864
        signatures: {},
865
      };
866
      const termsHash = existing?.termsHash ?? canonicalHash(contract);
867
      const createdAt = now.toISOString();
868
      const interaction: FederatedInteraction = {
869
        protocolVersion: '0.1',
870
        interactionId,
871
        initiatorAgentId: binding.agent.agentId,
872
        responderAgentId: responderCard.agentId,
873
        status: 'pending',
874
        contract,
875
        termsHash,
876
        acceptances: {
877
          [binding.agent.agentId]: {
878
            agentId: binding.agent.agentId,
879
            method: 'oauth_installation',
880
            termsHash,
881
            acceptedAt: createdAt,
882
          },
883
        },
884
        ...(initiatorCard.transports[0] ? { initiatorTransport: initiatorCard.transports[0] } : {}),
885
        responderTransport,
886
        createdAt,
887
        updatedAt: createdAt,
888
        expiresAt: new Date(now.getTime() + input.expiresInMinutes * 60_000).toISOString(),
889
      };
890
      const stored =
891
        existing ??
892
        (await agentDirectory.createFederatedInteraction(connection.operatorId, interaction));
893
      const delivery =
894
        stored.status === 'active'
895
          ? await agentDirectory.enqueueGatewayMessage({
896
              interactionId: stored.interactionId,
897
              senderAgentId: stored.initiatorAgentId,
898
              recipientAgentId: stored.responderAgentId,
899
              payload: {
900
                jsonrpc: '2.0',
901
                method: 'message/send',
902
                params: {
903
                  message: {
904
                    role: 'user',
905
                    parts: [{ kind: 'text', text: task }],
906
                    metadata: {
907
                      [DEFAULT_EXTENSION_URI]: {
908
                        interactionId: stored.interactionId,
909
                        termsHash: stored.termsHash,
910
                        initiatorAgentId: stored.initiatorAgentId,
911
                        responderAgentId: stored.responderAgentId,
912
                      },
913
                    },
914
                  },
915
                },
916
              },
917
              idempotencyKey: `initial:${stored.interactionId}`,
918
            })
919
          : undefined;
920
      return text({
921
        ready: stored.status === 'active',
922
        delivery,
923
        interaction: stored,
924
        a2a: {
925
          endpoint: stored.responderTransport.endpoint,
926
          bearerToken:
927
            stored.status === 'active' ? agentDirectory.issueGatewayToken(stored) : undefined,
928
          protocolBinding: stored.responderTransport.protocolBinding,
929
          extensions: [DEFAULT_EXTENSION_URI],
930
          metadata: {
931
            [DEFAULT_EXTENSION_URI]: {
932
              interactionId: stored.interactionId,
933
              termsHash: stored.termsHash,
934
              initiatorAgentId: stored.initiatorAgentId,
935
              responderAgentId: stored.responderAgentId,
936
            },
937
          },
938
          requestTemplate: {
939
            method: 'message/send',
940
            headers: { 'A2A-Extensions': DEFAULT_EXTENSION_URI },
941
            message: {
942
              role: 'user',
943
              parts: [{ kind: 'text', text: task }],
944
              metadata: {
945
                [DEFAULT_EXTENSION_URI]: {
946
                  interactionId: stored.interactionId,
947
                  termsHash: stored.termsHash,
948
                  initiatorAgentId: stored.initiatorAgentId,
949
                  responderAgentId: stored.responderAgentId,
950
                },
951
              },
952
            },
953
          },
954
        },
955
        next:
956
          stored.status === 'active'
957
            ? 'The task is queued for the other agent. Continue with openclasp_inbox and openclasp_send_message.'
958
            : `The task needs responder approval. OpenClasp will expose it at ${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/dashboard; retry this interaction after approval.`,
959
      });
960
    },
961
  );
962
  server.registerTool(
963
    OPENCLASP_TOOL_NAMES[25],
964
    {
965
      title: 'List shared invitations',
966
      description: 'List incoming and outgoing cross-account interaction invitations and status.',
967
      inputSchema: z.object({}),
968
      annotations: READ_ONLY_TOOL,
969
    },
970
    async (_input, context) => {
971
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
972
      await requireBoundAgent(context);
973
      const connection = installationContext(context);
974
      return text(await agentDirectory.listFederatedInteractions(connection.operatorId));
975
    },
976
  );
977
  server.registerTool(
978
    OPENCLASP_TOOL_NAMES[26],
979
    {
980
      title: 'Respond to agent invitation',
981
      description: 'Accept or reject an invitation as the agent bound to this MCP installation.',
982
      inputSchema: z.object({
983
        interactionId: z.string().uuid(),
984
        decision: z.enum(['accept', 'reject']),
985
      }),
986
      annotations: WRITE_TOOL,
987
    },
988
    async (input, context) => {
989
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
990
      const binding = await requireBoundAgent(context);
991
      if (!binding) throw new Error('A hosted, bound MCP installation is required');
992
      const connection = installationContext(context);
993
      return text(
994
        await agentDirectory.respondToFederatedInteraction(
995
          connection.operatorId,
996
          input.interactionId,
997
          binding.agent.agentId,
998
          input.decision,
999
          'oauth_installation',
1000
        ),
1001
      );
1002
    },
1003
  );
1004
  server.registerTool(
1005
    OPENCLASP_TOOL_NAMES[27],
1006
    {
1007
      title: 'Get shared interaction',
1008
      description: 'Get the canonical contract and bilateral acceptance state for one interaction.',
1009
      inputSchema: z.object({ interactionId: z.string().uuid() }),
1010
      annotations: READ_ONLY_TOOL,
1011
    },
1012
    async (input, context) => {
1013
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
1014
      await requireBoundAgent(context);
1015
      const connection = installationContext(context);
1016
      const value = await agentDirectory.getFederatedInteraction(
1017
        connection.operatorId,
1018
        input.interactionId,
1019
      );
1020
      if (!value) throw new Error('Interaction not found');
1021
      return text(value);
1022
    },
1023
  );
1024
  server.registerTool(
1025
    OPENCLASP_TOOL_NAMES[28],
1026
    {
1027
      title: 'Send an agent message',
1028
      description: 'Send a message to the other participant through the hosted A2A gateway.',
1029
      inputSchema: z.object({
1030
        interactionId: z.string().uuid(),
1031
        message: z.string().min(1).max(100_000),
1032
        contentType: z.string().max(100).default('text/plain'),
1033
      }),
1034
      annotations: WRITE_TOOL,
1035
    },
1036
    async (input, context) => {
1037
      if (!agentDirectory) throw new Error('The hosted A2A gateway is not configured');
1038
      const binding = await requireBoundAgent(context);
1039
      if (!binding) throw new Error('A bound MCP installation is required');
1040
      const connection = installationContext(context);
1041
      const interaction = await agentDirectory.getFederatedInteraction(
1042
        connection.operatorId,
1043
        input.interactionId,
1044
      );
1045
      if (!interaction || interaction.status !== 'active')
1046
        throw new Error('An active interaction is required');
1047
      const senderAgentId = binding.agent.agentId;
1048
      if (
1049
        senderAgentId !== interaction.initiatorAgentId &&
1050
        senderAgentId !== interaction.responderAgentId
1051
      )
1052
        throw new Error('The bound agent is not a participant in this interaction');
1053
      const recipientAgentId =
1054
        interaction.initiatorAgentId === senderAgentId
1055
          ? interaction.responderAgentId
1056
          : interaction.initiatorAgentId;
1057
      return text(
1058
        await agentDirectory.enqueueGatewayMessage({
1059
          interactionId: input.interactionId,
1060
          senderAgentId,
1061
          recipientAgentId,
1062
          payload: input.message,
1063
          contentType: input.contentType,
1064
        }),
1065
      );
1066
    },
1067
  );
1068
  server.registerTool(
1069
    OPENCLASP_TOOL_NAMES[29],
1070
    {
1071
      title: 'Read agent inbox',
1072
      description: 'Read queued messages addressed to this agent. Messages expire after 24 hours.',
1073
      inputSchema: z.object({ limit: z.number().int().min(1).max(50).default(20) }),
1074
      annotations: READ_ONLY_TOOL,
1075
    },
1076
    async (input, context) => {
1077
      if (!agentDirectory) throw new Error('The hosted A2A gateway is not configured');
1078
      const binding = await requireBoundAgent(context);
1079
      if (!binding) throw new Error('A bound MCP installation is required');
1080
      const connection = installationContext(context);
1081
      return text(
1082
        await agentDirectory.listGatewayMessages(
1083
          connection.operatorId,
1084
          binding.agent.agentId,
1085
          input.limit,
1086
        ),
1087
      );
1088
    },
1089
  );
1090
  server.registerTool(
1091
    OPENCLASP_TOOL_NAMES[30],
1092
    {
1093
      title: 'Acknowledge agent message',
1094
      description: 'Delete a gateway message after this agent has handled it.',
1095
      inputSchema: z.object({ messageId: z.string().uuid() }),
1096
      annotations: { ...WRITE_TOOL, destructiveHint: true, idempotentHint: true },
1097
    },
1098
    async (input, context) => {
1099
      if (!agentDirectory) throw new Error('The hosted A2A gateway is not configured');
1100
      const binding = await requireBoundAgent(context);
1101
      if (!binding) throw new Error('A bound MCP installation is required');
1102
      const connection = installationContext(context);
1103
      return text(
1104
        await agentDirectory.acknowledgeGatewayMessage(
1105
          connection.operatorId,
1106
          binding.agent.agentId,
1107
          input.messageId,
1108
        ),
1109
      );
1110
    },
1111
  );
1112
  return server;
1113
}