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

Viewing files at commit b506ef3834f7Commit detailsView latest

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