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

Viewing files at commit 2307e7ec1ffeCommit detailsView latest

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