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

Viewing files at commit 9a8fdf2387ffCommit detailsView latest

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