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

Viewing files at commit 479efea741d5Commit detailsView latest

packages/mcp-server/src/server.ts 1496 lines · 58.3 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
  InteractionCompletionReportSchema,
11
  InteractionFeedbackSchema,
12
  InteractionEventSchema,
13
  LiveSessionEventSchema,
14
  ReceiptSchema,
15
  TrustEnvelopeSchema,
16
  canonicalHash,
17
  type FederatedInteraction,
18
  type PublicAgentCard,
19
} from '../../protocol/src/index.js';
20
import {
21
  requestAgentSetup,
22
  resolveInstallation,
23
  updateAgentProfile,
24
  type OnboardingStore,
25
} from '../../persistence/src/onboarding.js';
26
import { buildPublicAgentCard } from '../../persistence/src/hosted.js';
27
28
export const OPENCLASP_TOOL_NAMES = [
29
  'openclasp_create_identity',
30
  'openclasp_register_agent',
31
  'openclasp_get_profile',
32
  'openclasp_assess_counterparty',
33
  'openclasp_begin_interaction',
34
  'openclasp_record_event',
35
  'openclasp_check_claim',
36
  'openclasp_validate_commitment',
37
  'openclasp_suggest_resolution',
38
  'openclasp_complete_interaction',
39
  'openclasp_submit_feedback',
40
  'openclasp_raise_dispute',
41
  'openclasp_verify_receipt',
42
  'openclasp_setup',
43
  'openclasp_get_identity',
44
  'openclasp_switch_agent',
45
  'openclasp_update_profile',
46
  'openclasp_connection_status',
47
  'openclasp_register_delegation',
48
  'openclasp_save_contract',
49
  'openclasp_permit_mediation',
50
  'openclasp_resolve_dispute',
51
  'openclasp_find_agent',
52
  'openclasp_search_agents',
53
  'openclasp_connect_to_agent',
54
  'openclasp_list_invitations',
55
  'openclasp_respond_invitation',
56
  'openclasp_get_shared_interaction',
57
  'openclasp_get_live_session',
58
  'openclasp_record_session_event',
59
  'openclasp_complete_live_session',
60
  'openclasp_heartbeat',
61
  'openclasp_list_threads',
62
  'openclasp_get_thread',
63
  'openclasp_send_message',
64
  'openclasp_reply',
65
  'openclasp_mark_read',
66
  'openclasp_close_thread',
67
  'openclasp_submit_completion_report',
68
  'openclasp_list_feedback_requests',
69
  'openclasp_submit_interaction_feedback',
70
] as const;
71
72
export const HOSTED_OPENCLASP_TOOL_NAMES = OPENCLASP_TOOL_NAMES.filter(
73
  (name) => name !== 'openclasp_create_identity',
74
);
75
76
const READ_ONLY_TOOL = {
77
  readOnlyHint: true,
78
  destructiveHint: false,
79
  idempotentHint: true,
80
} as const;
81
const WRITE_TOOL = {
82
  readOnlyHint: false,
83
  destructiveHint: false,
84
  idempotentHint: false,
85
} as const;
86
87
const text = (value: unknown) => ({
88
  content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
89
});
90
91
export const OPENCLASP_MCP_INSTRUCTIONS =
92
  'Call openclasp_connection_status and openclasp_heartbeat. Persistent runtimes use direct A2A. Temporary agents use openclasp_list_threads, openclasp_get_thread, openclasp_send_message, and openclasp_reply. At a terminal outcome, call openclasp_submit_completion_report, then openclasp_submit_interaction_feedback for your pending request. Submit evidence-backed structured facts only; never upload transcripts or invent peer feedback.';
93
94
export function buildMcpServer(engine = new TrustEngine()) {
95
  const server = new McpServer(
96
    { name: 'openclasp', version: '0.1.0' },
97
    {
98
      instructions: OPENCLASP_MCP_INSTRUCTIONS,
99
    },
100
  );
101
  return registerOpenClaspTools(server, engine);
102
}
103
104
type HostedRecord = (
105
  operatorId: string,
106
  kind:
107
    | 'agent'
108
    | 'delegation'
109
    | 'contract'
110
    | 'interaction'
111
    | 'event'
112
    | 'receipt'
113
    | 'feedback'
114
    | 'conflict'
115
    | 'profile',
116
  recordId: string,
117
  value: unknown,
118
) => Promise<void>;
119
120
type ToolContext = {
121
  http?: {
122
    authInfo?: { clientId?: string; extra?: Record<string, unknown> };
123
  };
124
};
125
type EngineSource = TrustEngine | ((context: ToolContext) => Promise<TrustEngine>);
126
type AgentDirectory = {
127
  publishAgent?(operatorId: string, card: PublicAgentCard): Promise<PublicAgentCard>;
128
  getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined>;
129
  searchPublishedAgents(input: {
130
    query?: string | undefined;
131
    capability?: string | undefined;
132
    limit?: number | undefined;
133
  }): Promise<PublicAgentCard[]>;
134
  createFederatedInteraction(
135
    operatorId: string,
136
    value: FederatedInteraction,
137
  ): Promise<FederatedInteraction>;
138
  listFederatedInteractions(operatorId: string): Promise<FederatedInteraction[]>;
139
  getFederatedInteraction(
140
    operatorId: string,
141
    interactionId: string,
142
  ): Promise<FederatedInteraction | undefined>;
143
  respondToFederatedInteraction(
144
    operatorId: string,
145
    interactionId: string,
146
    agentId: string,
147
    decision: 'accept' | 'reject',
148
    method?: 'oauth_installation' | 'oauth_account' | 'policy_auto_accept',
149
  ): Promise<FederatedInteraction>;
150
  getLiveSession(operatorId: string, interactionId: string, agentId: string): Promise<any>;
151
  recordLiveSessionEvent(
152
    token: string,
153
    event: z.infer<typeof LiveSessionEventSchema>,
154
  ): Promise<unknown>;
155
  submitCompletionReport?(
156
    operatorId: string,
157
    agentId: string,
158
    report: z.infer<typeof InteractionCompletionReportSchema>,
159
    submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
160
  ): Promise<unknown>;
161
  listFeedbackRequests?(operatorId: string, agentId: string): Promise<unknown[]>;
162
  submitInteractionFeedback?(
163
    operatorId: string,
164
    agentId: string,
165
    feedback: z.infer<typeof InteractionFeedbackSchema>,
166
    submissionMethod: 'oauth_installation' | 'agent_access_token' | 'runtime_session',
167
  ): Promise<unknown>;
168
  touchAgentPresence(operatorId: string, agentId: string): Promise<unknown>;
169
  listHostedThreads(operatorId: string, agentId: string): Promise<any[]>;
170
  getHostedThread(operatorId: string, agentId: string, threadId: string): Promise<any>;
171
  sendTemporaryMessage(
172
    operatorId: string,
173
    agentId: string,
174
    interactionId: string,
175
    content: string,
176
  ): Promise<any>;
177
  markHostedThreadRead(operatorId: string, agentId: string, threadId: string): Promise<any>;
178
  closeHostedThread(operatorId: string, agentId: string, threadId: string): Promise<any>;
179
};
180
181
function installationContext(context: ToolContext) {
182
  const operatorId = context.http?.authInfo?.extra?.operatorId;
183
  const clientId = context.http?.authInfo?.clientId;
184
  if (typeof operatorId !== 'string' || typeof clientId !== 'string')
185
    throw new Error('Authenticated MCP installation context is required');
186
  const boundAgentId = context.http?.authInfo?.extra?.boundAgentId;
187
  const credentialType = context.http?.authInfo?.extra?.credentialType;
188
  return {
189
    operatorId,
190
    clientId,
191
    ...(typeof boundAgentId === 'string' ? { boundAgentId } : {}),
192
    ...(typeof credentialType === 'string' ? { credentialType } : {}),
193
  };
194
}
195
196
export function registerOpenClaspTools(
197
  server: McpServer,
198
  engineSource: EngineSource = new TrustEngine(),
199
  recordHosted?: HostedRecord,
200
  onboardingStore?: OnboardingStore,
201
  agentDirectory?: AgentDirectory,
202
) {
203
  const factChecker = new FixtureFactCheckProvider();
204
  const resolveEngine = (context: ToolContext) =>
205
    typeof engineSource === 'function' ? engineSource(context) : Promise.resolve(engineSource);
206
  const persist = async (
207
    context: ToolContext,
208
    kind: Parameters<HostedRecord>[1],
209
    id: string,
210
    value: unknown,
211
  ) => {
212
    const operatorId = context.http?.authInfo?.extra?.operatorId;
213
    if (recordHosted && typeof operatorId === 'string')
214
      await recordHosted(operatorId, kind, id, value);
215
  };
216
217
  const resolveAuthenticatedInstallation = async (context: ToolContext) => {
218
    if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
219
    const connection = installationContext(context);
220
    const binding = await resolveInstallation(
221
      onboardingStore,
222
      connection.operatorId,
223
      connection.clientId,
224
    );
225
    if (
226
      connection.boundAgentId &&
227
      (binding.status !== 'connected' || binding.agent.agentId !== connection.boundAgentId)
228
    )
229
      throw new Error('Agent access token binding is invalid');
230
    return { connection, binding };
231
  };
232
233
  const requireBoundAgent = async (context: ToolContext, claimedAgentId?: string) => {
234
    if (!onboardingStore) return undefined;
235
    const { connection, binding } = await resolveAuthenticatedInstallation(context);
236
    if (binding.status !== 'connected')
237
      throw new Error('Call openclasp_setup and obtain owner confirmation before using this tool');
238
    if (claimedAgentId && binding.agent.agentId !== claimedAgentId)
239
      throw new Error('The claimed agent does not match this MCP installation');
240
    if (agentDirectory)
241
      await agentDirectory.touchAgentPresence(connection.operatorId, binding.agent.agentId);
242
    return binding;
243
  };
244
245
  if (!onboardingStore)
246
    server.registerTool(
247
      OPENCLASP_TOOL_NAMES[0],
248
      {
249
        title: 'Create local cryptographic identity',
250
        description:
251
          'Create an Ed25519 identity for local development. This returns private key material and is intentionally unavailable on hosted MCP.',
252
        inputSchema: z.object({
253
          agentId: z.string(),
254
          operatorRef: z.string(),
255
          capabilities: z.array(z.string()),
256
        }),
257
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
258
      },
259
      async (input, context) => {
260
        const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
261
        const created = createIdentity({
262
          ...input,
263
          operatorRef:
264
            typeof authenticatedOperator === 'string'
265
              ? `auth0:${authenticatedOperator}`
266
              : input.operatorRef,
267
        });
268
        return text(created);
269
      },
270
    );
271
  server.registerTool(
272
    OPENCLASP_TOOL_NAMES[1],
273
    {
274
      title: 'Register cryptographic identity',
275
      description: 'Register and verify a signed agent identity',
276
      inputSchema: AgentIdentitySchema,
277
      annotations: { ...WRITE_TOOL, idempotentHint: true },
278
    },
279
    async (input, context) => {
280
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
281
      if (
282
        typeof authenticatedOperator === 'string' &&
283
        input.operatorRef !== `auth0:${authenticatedOperator}`
284
      ) {
285
        throw new Error('Agent identity is not owned by the authenticated operator');
286
      }
287
      await requireBoundAgent(context, input.agentId);
288
      const engine = await resolveEngine(context);
289
      const registered = engine.registerAgent(input);
290
      await persist(context, 'agent', registered.agentId, registered);
291
      return text(registered);
292
    },
293
  );
294
  server.registerTool(
295
    OPENCLASP_TOOL_NAMES[2],
296
    {
297
      title: 'Get contextual reliability',
298
      description: 'Get a task-specific behavioural profile',
299
      inputSchema: z.object({ agentId: z.string(), version: z.string(), taskCategory: z.string() }),
300
      annotations: READ_ONLY_TOOL,
301
    },
302
    async (input, context) => {
303
      await requireBoundAgent(context);
304
      const engine = await resolveEngine(context);
305
      return text(engine.getRisk(input.agentId, input.version, input.taskCategory));
306
    },
307
  );
308
  server.registerTool(
309
    OPENCLASP_TOOL_NAMES[3],
310
    {
311
      title: 'Assess counterparty',
312
      description: 'Assess a counterparty in context',
313
      inputSchema: z.object({
314
        envelope: TrustEnvelopeSchema,
315
        action: z.string(),
316
        dataClasses: z.array(z.string()).optional(),
317
        humanApproved: z.boolean().optional(),
318
      }),
319
      annotations: READ_ONLY_TOOL,
320
    },
321
    async (input, context) => {
322
      await requireBoundAgent(context);
323
      const engine = await resolveEngine(context);
324
      return text(engine.assess(input as any));
325
    },
326
  );
327
  server.registerTool(
328
    OPENCLASP_TOOL_NAMES[4],
329
    {
330
      title: 'Begin assured interaction',
331
      description: 'Begin an interaction and return its identifier',
332
      inputSchema: z.object({
333
        purpose: z.string(),
334
        parties: z.array(z.string()),
335
        taskCategory: z.string(),
336
      }),
337
      annotations: WRITE_TOOL,
338
    },
339
    async (input, context) => {
340
      const binding = await requireBoundAgent(context);
341
      if (binding && !input.parties.includes(binding.agent.agentId))
342
        throw new Error('Interaction parties must include the agent bound to this installation');
343
      const interaction = {
344
        interactionId: crypto.randomUUID(),
345
        status: 'contract_required',
346
        createdAt: new Date().toISOString(),
347
        ...input,
348
      };
349
      await persist(context, 'interaction', interaction.interactionId, interaction);
350
      return text(interaction);
351
    },
352
  );
353
  server.registerTool(
354
    OPENCLASP_TOOL_NAMES[5],
355
    {
356
      title: 'Record signed event',
357
      description: 'Validate and record a signed structured event',
358
      inputSchema: InteractionEventSchema,
359
      annotations: { ...WRITE_TOOL, idempotentHint: true },
360
    },
361
    async (input, context) => {
362
      await requireBoundAgent(context, input.agentId);
363
      const engine = await resolveEngine(context);
364
      const event = engine.recordEvent(input);
365
      await persist(context, 'event', event.eventId, event);
366
      return text(event);
367
    },
368
  );
369
  server.registerTool(
370
    OPENCLASP_TOOL_NAMES[6],
371
    {
372
      title: 'Check factual claim',
373
      description: 'Check an objective factual claim',
374
      inputSchema: z.object({ claim: z.string(), permission: z.boolean().optional() }),
375
      annotations: { ...READ_ONLY_TOOL, openWorldHint: true },
376
    },
377
    async (input) => text(await factChecker.check(input.claim, input.permission)),
378
  );
379
  server.registerTool(
380
    OPENCLASP_TOOL_NAMES[7],
381
    {
382
      title: 'Validate commitment',
383
      description: 'Check whether a commitment is represented by a signed event',
384
      inputSchema: z.object({ interactionId: z.string().uuid(), commitment: z.string() }),
385
      annotations: READ_ONLY_TOOL,
386
    },
387
    async (input, context) => {
388
      await requireBoundAgent(context);
389
      const engine = await resolveEngine(context);
390
      return text({
391
        valid: [...engine.events.values()].some(
392
          (event) =>
393
            event.interactionId === input.interactionId &&
394
            event.eventType === 'commitment' &&
395
            JSON.stringify(event.payload).includes(input.commitment),
396
        ),
397
      });
398
    },
399
  );
400
  server.registerTool(
401
    OPENCLASP_TOOL_NAMES[8],
402
    {
403
      title: 'Suggest dispute resolution',
404
      description: 'Suggest attributable resolutions for a consented conflict',
405
      inputSchema: z.object({ conflictId: z.string() }),
406
      annotations: READ_ONLY_TOOL,
407
    },
408
    async (input, context) => {
409
      const binding = await requireBoundAgent(context);
410
      const engine = await resolveEngine(context);
411
      const conflict = engine.conflicts.get(input.conflictId);
412
      if (binding && (!conflict || !(binding.agent.agentId in conflict.permissions)))
413
        throw new Error('The bound agent is not a participant in this dispute');
414
      return text(engine.conflicts.get(input.conflictId)?.possibleResolutions ?? []);
415
    },
416
  );
417
  server.registerTool(
418
    OPENCLASP_TOOL_NAMES[9],
419
    {
420
      title: 'Complete interaction',
421
      description: 'Submit a signed completion receipt',
422
      inputSchema: ReceiptSchema,
423
      annotations: { ...WRITE_TOOL, idempotentHint: true },
424
    },
425
    async (input, context) => {
426
      const binding = await requireBoundAgent(context);
427
      if (binding && !input.participants.includes(binding.agent.agentId))
428
        throw new Error('Receipt participants must include the bound agent');
429
      const engine = await resolveEngine(context);
430
      const receipt = engine.submitReceipt(input);
431
      await persist(context, 'receipt', receipt.receiptId, receipt);
432
      return text(receipt);
433
    },
434
  );
435
  server.registerTool(
436
    OPENCLASP_TOOL_NAMES[10],
437
    {
438
      title: 'Submit bilateral feedback',
439
      description: 'Submit receipt-backed structured feedback',
440
      inputSchema: FeedbackSchema,
441
      annotations: { ...WRITE_TOOL, idempotentHint: true },
442
    },
443
    async (input, context) => {
444
      await requireBoundAgent(context, input.reviewerAgentId);
445
      const engine = await resolveEngine(context);
446
      const result = engine.submitFeedback(input);
447
      await persist(context, 'feedback', input.feedbackId, input);
448
      for (const profile of engine.profiles.values())
449
        await persist(
450
          context,
451
          'profile',
452
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
453
          profile,
454
        );
455
      return text(result);
456
    },
457
  );
458
  server.registerTool(
459
    OPENCLASP_TOOL_NAMES[11],
460
    {
461
      title: 'Raise dispute',
462
      description: 'Create a mutually consented dispute',
463
      inputSchema: z.object({
464
        interactionId: z.string().uuid(),
465
        issue: z.string(),
466
        participants: z.array(z.string()).min(2),
467
      }),
468
      annotations: WRITE_TOOL,
469
    },
470
    async (input, context) => {
471
      const binding = await requireBoundAgent(context);
472
      if (binding && !input.participants.includes(binding.agent.agentId))
473
        throw new Error('Dispute participants must include the bound agent');
474
      const engine = await resolveEngine(context);
475
      const conflict = engine.createConflict({
476
        interactionId: input.interactionId,
477
        issue: input.issue,
478
        participants: input.participants,
479
        positions: {},
480
        evidence: [],
481
        contractClauses: [],
482
        missingInformation: [],
483
        possibleResolutions: [],
484
      });
485
      await persist(context, 'conflict', conflict.conflictId, conflict);
486
      return text(conflict);
487
    },
488
  );
489
  server.registerTool(
490
    OPENCLASP_TOOL_NAMES[12],
491
    {
492
      title: 'Verify receipt',
493
      description: 'Verify a signed interaction receipt',
494
      inputSchema: ReceiptSchema,
495
      annotations: READ_ONLY_TOOL,
496
    },
497
    async (input, context) => {
498
      await requireBoundAgent(context);
499
      const engine = await resolveEngine(context);
500
      try {
501
        engine.verifyReceipt(input);
502
        return text({ valid: true });
503
      } catch {
504
        return text({ valid: false });
505
      }
506
    },
507
  );
508
  server.registerTool(
509
    OPENCLASP_TOOL_NAMES[13],
510
    {
511
      title: 'Set up this agent',
512
      description:
513
        'Propose this installation and safe automation policy. Temporary chats receive an OpenClasp-hosted A2A endpoint; persistent agents connect their own runtime.',
514
      inputSchema: z
515
        .object({
516
          agentName: z.string().trim().min(1).max(100),
517
          projectName: z.string().trim().min(1).max(100).optional(),
518
          projectId: z.string().optional(),
519
          framework: z.string().trim().max(100).optional(),
520
          description: z.string().trim().max(500).optional(),
521
          agentVersion: z.string().trim().min(1).max(100).optional(),
522
          agentMode: z.enum(['persistent_runtime', 'temporary_chat']).optional(),
523
          autoPublish: z.boolean().optional(),
524
          autoAcceptPolicy: z.enum(['off', 'safe_matching']).optional(),
525
          autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
526
          capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
527
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
528
        })
529
        .refine((input) => input.projectName || input.projectId, {
530
          message: 'projectName or projectId is required',
531
        }),
532
      annotations: WRITE_TOOL,
533
    },
534
    async (input, context) => {
535
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
536
      const { connection, binding: current } = await resolveAuthenticatedInstallation(context);
537
      if (current.status === 'connected') {
538
        await agentDirectory?.touchAgentPresence(connection.operatorId, current.agent.agentId);
539
        return text(current);
540
      }
541
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
542
        clientId: connection.clientId,
543
        ...input,
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
        next: 'Ask the owner to confirm this setup request in the OpenClasp dashboard, then call openclasp_get_identity.',
551
      });
552
    },
553
  );
554
  server.registerTool(
555
    OPENCLASP_TOOL_NAMES[14],
556
    {
557
      title: 'Get my agent identity',
558
      description:
559
        'Return the agent identity automatically bound to this authenticated MCP installation.',
560
      inputSchema: z.object({}),
561
      annotations: READ_ONLY_TOOL,
562
    },
563
    async (_input, context) => {
564
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
565
      const { connection, binding } = await resolveAuthenticatedInstallation(context);
566
      if (binding.status === 'connected')
567
        await agentDirectory?.touchAgentPresence(connection.operatorId, binding.agent.agentId);
568
      return text(binding);
569
    },
570
  );
571
  server.registerTool(
572
    OPENCLASP_TOOL_NAMES[15],
573
    {
574
      title: 'Switch agent identity',
575
      description:
576
        'Request that this MCP installation switch to another existing agent. Dashboard confirmation is required.',
577
      inputSchema: z.object({ existingAgentId: z.string().min(1) }),
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
      if (connection.credentialType === 'agent_access_token')
584
        throw new Error('Agent access tokens cannot switch agent identity');
585
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
586
        clientId: connection.clientId,
587
        action: 'switch',
588
        existingAgentId: input.existingAgentId,
589
      });
590
      return text({
591
        status: 'pending_confirmation',
592
        confirmationRequired: true,
593
        request,
594
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
595
      });
596
    },
597
  );
598
  server.registerTool(
599
    OPENCLASP_TOOL_NAMES[16],
600
    {
601
      title: 'Update my agent profile',
602
      description: 'Update the profile of the agent bound to this MCP installation.',
603
      inputSchema: z.object({
604
        name: z.string().trim().min(1).max(100).optional(),
605
        framework: z.string().trim().min(1).max(100).optional(),
606
        description: z.string().trim().max(500).optional(),
607
        agentVersion: z.string().trim().min(1).max(100).optional(),
608
        agentMode: z.enum(['persistent_runtime', 'temporary_chat']).optional(),
609
        autoPublish: z.boolean().optional(),
610
        autoAcceptPolicy: z.enum(['off', 'safe_matching']).optional(),
611
        autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
612
        capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
613
        limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
614
      }),
615
      annotations: WRITE_TOOL,
616
    },
617
    async (input, context) => {
618
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
619
      const { connection } = await resolveAuthenticatedInstallation(context);
620
      const agent = await updateAgentProfile(
621
        onboardingStore,
622
        connection.operatorId,
623
        connection.clientId,
624
        input,
625
      );
626
      const published = await agentDirectory?.getPublishedAgent(agent.agentId);
627
      if (published && agentDirectory?.publishAgent)
628
        await agentDirectory.publishAgent(
629
          connection.operatorId,
630
          buildPublicAgentCard(
631
            agent,
632
            process.env.OPENCLASP_PUBLIC_URL ??
633
              process.env.OPENCLASP_DASHBOARD_URL ??
634
              'https://openclasp.vercel.app',
635
            published,
636
          ),
637
        );
638
      return text(agent);
639
    },
640
  );
641
  server.registerTool(
642
    OPENCLASP_TOOL_NAMES[17],
643
    {
644
      title: 'Check OpenClasp connection',
645
      description:
646
        'Check whether this MCP installation is unbound, awaiting confirmation, or connected.',
647
      inputSchema: z.object({}),
648
      annotations: READ_ONLY_TOOL,
649
    },
650
    async (_input, context) => {
651
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
652
      const { connection, binding } = await resolveAuthenticatedInstallation(context);
653
      if (binding.status === 'connected') {
654
        await agentDirectory?.touchAgentPresence(connection.operatorId, binding.agent.agentId);
655
        return text(binding);
656
      }
657
      const state = await onboardingStore.list(connection.operatorId);
658
      const pending = state
659
        .filter((row) => row.kind === 'setup_request')
660
        .map((row) => row.payload as { clientId?: string; status?: string })
661
        .find(
662
          (request) => request.clientId === connection.clientId && request.status === 'pending',
663
        );
664
      return text(
665
        pending
666
          ? {
667
              status: 'pending_confirmation',
668
              confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
669
            }
670
          : binding,
671
      );
672
    },
673
  );
674
  server.registerTool(
675
    OPENCLASP_TOOL_NAMES[18],
676
    {
677
      title: 'Register delegation',
678
      description: 'Register and verify a signed, scoped delegation between two known agents.',
679
      inputSchema: DelegationCredentialSchema,
680
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
681
    },
682
    async (input, context) => {
683
      const binding = await requireBoundAgent(context);
684
      if (
685
        binding &&
686
        input.parentAgentId !== binding.agent.agentId &&
687
        input.childAgentId !== binding.agent.agentId
688
      )
689
        throw new Error('The bound agent is not a party to this delegation');
690
      const engine = await resolveEngine(context);
691
      engine.delegations.set(input.delegationId, input);
692
      if (!engine.verifyDelegation(input.delegationId)) {
693
        engine.delegations.delete(input.delegationId);
694
        throw new Error('Delegation verification failed');
695
      }
696
      await persist(context, 'delegation', input.delegationId, input);
697
      return text({ valid: true, delegation: input });
698
    },
699
  );
700
  server.registerTool(
701
    OPENCLASP_TOOL_NAMES[19],
702
    {
703
      title: 'Save signed agreement',
704
      description:
705
        'Verify and save a complete interaction contract signed by every participating agent.',
706
      inputSchema: InteractionContractSchema,
707
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
708
    },
709
    async (input, context) => {
710
      const binding = await requireBoundAgent(context);
711
      if (binding && !input.parties.includes(binding.agent.agentId))
712
        throw new Error('Contract parties must include the bound agent');
713
      const contract = (await resolveEngine(context)).saveContract(input);
714
      await persist(context, 'contract', contract.interactionId, contract);
715
      return text(contract);
716
    },
717
  );
718
  server.registerTool(
719
    OPENCLASP_TOOL_NAMES[20],
720
    {
721
      title: 'Consent to mediation',
722
      description:
723
        "Record the bound agent's explicit consent to mediation for a dispute it participates in.",
724
      inputSchema: z.object({ conflictId: z.string(), agentId: z.string().optional() }),
725
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
726
    },
727
    async (input, context) => {
728
      const binding = await requireBoundAgent(context);
729
      const agentId = binding?.agent.agentId ?? input.agentId;
730
      if (!agentId) throw new Error('agentId is required for local MCP');
731
      const engine = await resolveEngine(context);
732
      const conflict = engine.permitMediation(input.conflictId, agentId);
733
      await persist(context, 'conflict', conflict.conflictId, conflict);
734
      return text(conflict);
735
    },
736
  );
737
  server.registerTool(
738
    OPENCLASP_TOOL_NAMES[21],
739
    {
740
      title: 'Resolve mediated dispute',
741
      description:
742
        'Save an agreed resolution only after every dispute participant has consented to mediation.',
743
      inputSchema: z.object({ conflictId: z.string(), resolution: z.string().trim().min(1) }),
744
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
745
    },
746
    async (input, context) => {
747
      const binding = await requireBoundAgent(context);
748
      const engine = await resolveEngine(context);
749
      const existing = engine.conflicts.get(input.conflictId);
750
      if (binding && (!existing || !(binding.agent.agentId in existing.permissions)))
751
        throw new Error('The bound agent is not a participant in this dispute');
752
      const conflict = engine.resolveConflict(input.conflictId, input.resolution);
753
      await persist(context, 'conflict', conflict.conflictId, conflict);
754
      return text(conflict);
755
    },
756
  );
757
  server.registerTool(
758
    OPENCLASP_TOOL_NAMES[22],
759
    {
760
      title: 'Find published agent',
761
      description:
762
        'Find an exact owner-published agent card. Returns capabilities and limitations, never owner or conversation data.',
763
      inputSchema: z.object({ agentId: z.string().min(1) }),
764
      annotations: READ_ONLY_TOOL,
765
    },
766
    async (input, context) => {
767
      await requireBoundAgent(context);
768
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
769
      return text((await agentDirectory.getPublishedAgent(input.agentId)) ?? { found: false });
770
    },
771
  );
772
  server.registerTool(
773
    OPENCLASP_TOOL_NAMES[23],
774
    {
775
      title: 'Search published agents',
776
      description:
777
        'Search owner-published agent cards by name, framework, or capability without exposing owner identities or projects.',
778
      inputSchema: z.object({
779
        query: z.string().trim().max(100).optional(),
780
        capability: z.string().trim().max(100).optional(),
781
        limit: z.number().int().min(1).max(50).optional(),
782
      }),
783
      annotations: READ_ONLY_TOOL,
784
    },
785
    async (input, context) => {
786
      await requireBoundAgent(context);
787
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
788
      return text(await agentDirectory.searchPublishedAgents(input));
789
    },
790
  );
791
  server.registerTool(
792
    OPENCLASP_TOOL_NAMES[24],
793
    {
794
      title: 'Automatically connect to another agent',
795
      description:
796
        'Infer safe contract defaults, auto-activate when the responder policy permits, and return a ready-to-send A2A request.',
797
      inputSchema: z
798
        .object({
799
          targetAgentId: z.string().min(1).optional(),
800
          targetAgentCardUrl: z.string().url().optional(),
801
          task: z.string().trim().min(1).max(2000).optional(),
802
          purpose: z.string().trim().min(1).max(500).optional(),
803
          taskCategory: z.string().trim().min(1).max(100).optional(),
804
          requestedOutcome: z.string().trim().min(1).max(1000).optional(),
805
          successCriteria: z.array(z.string().trim().min(1)).min(1).max(50).optional(),
806
          allowedActions: z.array(z.string().trim().min(1)).max(100).default([]),
807
          prohibitedActions: z.array(z.string().trim().min(1)).max(100).default([]),
808
          allowedData: z.array(z.string().trim().min(1)).max(100).default([]),
809
          prohibitedData: z.array(z.string().trim().min(1)).max(100).default([]),
810
          evidenceRequirements: z.array(z.string().trim().min(1)).max(100).default([]),
811
          deadline: z.string().datetime().optional(),
812
          expiresInMinutes: z.number().int().min(5).max(10080).default(60),
813
        })
814
        .superRefine((value, context) => {
815
          if (!value.targetAgentId && !value.targetAgentCardUrl)
816
            context.addIssue({
817
              code: 'custom',
818
              message: 'targetAgentId or targetAgentCardUrl is required',
819
            });
820
          if (!value.task && !value.purpose)
821
            context.addIssue({ code: 'custom', message: 'task or purpose is required' });
822
        }),
823
      annotations: { ...WRITE_TOOL, openWorldHint: true },
824
    },
825
    async (input, context) => {
826
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
827
      const binding = await requireBoundAgent(context);
828
      if (!binding) throw new Error('A hosted, bound MCP installation is required');
829
      const connection = installationContext(context);
830
      let initiatorCard = await agentDirectory.getPublishedAgent(binding.agent.agentId);
831
      if (!initiatorCard && binding.agent.autoPublish && agentDirectory.publishAgent)
832
        initiatorCard = await agentDirectory.publishAgent(
833
          connection.operatorId,
834
          buildPublicAgentCard(
835
            binding.agent,
836
            process.env.OPENCLASP_PUBLIC_URL ??
837
              process.env.OPENCLASP_DASHBOARD_URL ??
838
              'https://openclasp.vercel.app',
839
          ),
840
        );
841
      if (!initiatorCard)
842
        throw new Error(
843
          'This agent is private. Enable automatic publishing once in its dashboard settings.',
844
        );
845
      let targetAgentId = input.targetAgentId;
846
      if (!targetAgentId && input.targetAgentCardUrl) {
847
        const cardUrl = new URL(input.targetAgentCardUrl);
848
        const trustedOrigin = new URL(
849
          process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app',
850
        ).origin;
851
        const match = cardUrl.pathname.match(/^\/agents\/([^/]+)\/card\.json$/);
852
        if (cardUrl.origin !== trustedOrigin || !match?.[1])
853
          throw new Error('Only OpenClasp-hosted Agent Card URLs are accepted');
854
        targetAgentId = decodeURIComponent(match[1]);
855
      }
856
      if (!targetAgentId) throw new Error('Target agent is required');
857
      const responderCard = await agentDirectory.getPublishedAgent(targetAgentId);
858
      if (!responderCard) throw new Error('Target agent is not published on OpenClasp');
859
      if (
860
        initiatorCard.agentMode === 'temporary_chat' &&
861
        responderCard.agentMode === 'temporary_chat'
862
      )
863
        throw new Error('Temporary-to-temporary conversations are not supported in this MVP');
864
      const responderTransport = responderCard.transports[0];
865
      if (!responderTransport) throw new Error('Target agent has not published an A2A endpoint');
866
      const task = input.task ?? input.purpose!;
867
      const purpose = input.purpose ?? task.slice(0, 500);
868
      const taskCategory = input.taskCategory ?? responderCard.capabilities[0] ?? 'general';
869
      const requestedOutcome = input.requestedOutcome ?? task.slice(0, 1000);
870
      const successCriteria = input.successCriteria ?? [
871
        'Return a clear result that directly addresses the requested task',
872
      ];
873
      const existing = (await agentDirectory.listFederatedInteractions(connection.operatorId)).find(
874
        (candidate) =>
875
          candidate.initiatorAgentId === binding.agent.agentId &&
876
          candidate.responderAgentId === responderCard.agentId &&
877
          ['pending', 'active'].includes(candidate.status) &&
878
          candidate.contract.purpose === purpose &&
879
          candidate.contract.taskCategory === taskCategory &&
880
          candidate.contract.requestedOutcome === requestedOutcome &&
881
          JSON.stringify(candidate.contract.successCriteria) === JSON.stringify(successCriteria) &&
882
          JSON.stringify(candidate.contract.allowedActions) ===
883
            JSON.stringify(input.allowedActions) &&
884
          JSON.stringify(candidate.contract.prohibitedActions) ===
885
            JSON.stringify(input.prohibitedActions) &&
886
          JSON.stringify(candidate.contract.allowedData) === JSON.stringify(input.allowedData) &&
887
          JSON.stringify(candidate.contract.prohibitedData) ===
888
            JSON.stringify(input.prohibitedData) &&
889
          JSON.stringify(candidate.contract.evidenceRequirements) ===
890
            JSON.stringify(input.evidenceRequirements) &&
891
          candidate.contract.deadline === input.deadline,
892
      );
893
      const now = new Date();
894
      const interactionId = existing?.interactionId ?? crypto.randomUUID();
895
      const contract = {
896
        protocolVersion: '0.1' as const,
897
        interactionId,
898
        purpose,
899
        parties: [binding.agent.agentId, responderCard.agentId],
900
        taskCategory,
901
        requestedOutcome,
902
        successCriteria,
903
        allowedActions: input.allowedActions,
904
        prohibitedActions: input.prohibitedActions,
905
        allowedData: input.allowedData,
906
        prohibitedData: input.prohibitedData,
907
        ...(input.deadline ? { deadline: input.deadline } : {}),
908
        evidenceRequirements: input.evidenceRequirements,
909
        delegationRules: ['explicit_contract_scope'],
910
        humanApprovalRequirements: [],
911
        factCheckingPolicy: 'important_claims',
912
        mediationPolicy: 'mutual_consent' as const,
913
        retentionDays: 30,
914
        completionConditions: successCriteria,
915
        cancellationConditions: ['either_party_before_completion'],
916
        signatures: {},
917
      };
918
      const termsHash = existing?.termsHash ?? canonicalHash(contract);
919
      const createdAt = now.toISOString();
920
      const interaction: FederatedInteraction = {
921
        protocolVersion: '0.1',
922
        interactionId,
923
        initiatorAgentId: binding.agent.agentId,
924
        responderAgentId: responderCard.agentId,
925
        status: 'pending',
926
        contract,
927
        termsHash,
928
        acceptances: {
929
          [binding.agent.agentId]: {
930
            agentId: binding.agent.agentId,
931
            method: 'oauth_installation',
932
            termsHash,
933
            acceptedAt: createdAt,
934
          },
935
        },
936
        ...(initiatorCard.transports[0] ? { initiatorTransport: initiatorCard.transports[0] } : {}),
937
        responderTransport,
938
        createdAt,
939
        updatedAt: createdAt,
940
        expiresAt: new Date(now.getTime() + input.expiresInMinutes * 60_000).toISOString(),
941
      };
942
      const stored =
943
        existing ??
944
        (await agentDirectory.createFederatedInteraction(connection.operatorId, interaction));
945
      const session =
946
        stored.status === 'active'
947
          ? await agentDirectory.getLiveSession(
948
              connection.operatorId,
949
              stored.interactionId,
950
              binding.agent.agentId,
951
            )
952
          : undefined;
953
      return text({
954
        ready: stored.status === 'active',
955
        session,
956
        interaction: stored,
957
        a2a: {
958
          endpoint: session?.peer.endpoint,
959
          bearerToken: session?.peer.bearerToken,
960
          protocolBinding: stored.responderTransport.protocolBinding,
961
          extensions: [DEFAULT_EXTENSION_URI],
962
          metadata: {
963
            [DEFAULT_EXTENSION_URI]: {
964
              interactionId: stored.interactionId,
965
              termsHash: stored.termsHash,
966
              initiatorAgentId: stored.initiatorAgentId,
967
              responderAgentId: stored.responderAgentId,
968
            },
969
          },
970
          requestTemplate: {
971
            method: 'message/send',
972
            headers: { 'A2A-Extensions': DEFAULT_EXTENSION_URI },
973
            message: {
974
              role: 'user',
975
              parts: [{ kind: 'text', text: task }],
976
              metadata: {
977
                [DEFAULT_EXTENSION_URI]: {
978
                  interactionId: stored.interactionId,
979
                  termsHash: stored.termsHash,
980
                  initiatorAgentId: stored.initiatorAgentId,
981
                  responderAgentId: stored.responderAgentId,
982
                },
983
              },
984
            },
985
          },
986
        },
987
        next:
988
          stored.status === 'active'
989
            ? 'Both live runtimes accepted. Send the A2A request directly to session.peer.endpoint; OpenClasp is not in the message path.'
990
            : `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.`,
991
      });
992
    },
993
  );
994
  server.registerTool(
995
    OPENCLASP_TOOL_NAMES[25],
996
    {
997
      title: 'List shared invitations',
998
      description: 'List incoming and outgoing cross-account interaction invitations and status.',
999
      inputSchema: z.object({}),
1000
      annotations: READ_ONLY_TOOL,
1001
    },
1002
    async (_input, context) => {
1003
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
1004
      await requireBoundAgent(context);
1005
      const connection = installationContext(context);
1006
      return text(await agentDirectory.listFederatedInteractions(connection.operatorId));
1007
    },
1008
  );
1009
  server.registerTool(
1010
    OPENCLASP_TOOL_NAMES[26],
1011
    {
1012
      title: 'Respond to agent invitation',
1013
      description: 'Accept or reject an invitation as the agent bound to this MCP installation.',
1014
      inputSchema: z.object({
1015
        interactionId: z.string().uuid(),
1016
        decision: z.enum(['accept', 'reject']),
1017
      }),
1018
      annotations: WRITE_TOOL,
1019
    },
1020
    async (input, context) => {
1021
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
1022
      const binding = await requireBoundAgent(context);
1023
      if (!binding) throw new Error('A hosted, bound MCP installation is required');
1024
      const connection = installationContext(context);
1025
      return text(
1026
        await agentDirectory.respondToFederatedInteraction(
1027
          connection.operatorId,
1028
          input.interactionId,
1029
          binding.agent.agentId,
1030
          input.decision,
1031
          'oauth_installation',
1032
        ),
1033
      );
1034
    },
1035
  );
1036
  server.registerTool(
1037
    OPENCLASP_TOOL_NAMES[27],
1038
    {
1039
      title: 'Get shared interaction',
1040
      description: 'Get the canonical contract and bilateral acceptance state for one interaction.',
1041
      inputSchema: z.object({ interactionId: z.string().uuid() }),
1042
      annotations: READ_ONLY_TOOL,
1043
    },
1044
    async (input, context) => {
1045
      if (!agentDirectory) throw new Error('Federated agent connections are not configured');
1046
      await requireBoundAgent(context);
1047
      const connection = installationContext(context);
1048
      const value = await agentDirectory.getFederatedInteraction(
1049
        connection.operatorId,
1050
        input.interactionId,
1051
      );
1052
      if (!value) throw new Error('Interaction not found');
1053
      return text(value);
1054
    },
1055
  );
1056
  server.registerTool(
1057
    OPENCLASP_TOOL_NAMES[28],
1058
    {
1059
      title: 'Get direct live session',
1060
      description:
1061
        'Get the peer A2A endpoint and short-lived credential for a brokered live session.',
1062
      inputSchema: z.object({ interactionId: z.string().uuid() }),
1063
      annotations: READ_ONLY_TOOL,
1064
    },
1065
    async (input, context) => {
1066
      if (!agentDirectory) throw new Error('Live agent sessions are not configured');
1067
      const binding = await requireBoundAgent(context);
1068
      if (!binding) throw new Error('A bound MCP installation is required');
1069
      const connection = installationContext(context);
1070
      return text(
1071
        await agentDirectory.getLiveSession(
1072
          connection.operatorId,
1073
          input.interactionId,
1074
          binding.agent.agentId,
1075
        ),
1076
      );
1077
    },
1078
  );
1079
  server.registerTool(
1080
    OPENCLASP_TOOL_NAMES[29],
1081
    {
1082
      title: 'Record structured session event',
1083
      description:
1084
        'Record signed session metadata, hashes, evidence, corrections, or results without uploading raw messages.',
1085
      inputSchema: LiveSessionEventSchema.omit({ eventId: true, agentId: true }),
1086
      annotations: WRITE_TOOL,
1087
    },
1088
    async (input, context) => {
1089
      if (!agentDirectory) throw new Error('Live agent sessions are not configured');
1090
      const binding = await requireBoundAgent(context);
1091
      if (!binding) throw new Error('A bound MCP installation is required');
1092
      const connection = installationContext(context);
1093
      const session = await agentDirectory.getLiveSession(
1094
        connection.operatorId,
1095
        input.interactionId,
1096
        binding.agent.agentId,
1097
      );
1098
      return text(
1099
        await agentDirectory.recordLiveSessionEvent(session.reporting.bearerToken, {
1100
          ...input,
1101
          eventId: crypto.randomUUID(),
1102
          agentId: binding.agent.agentId,
1103
        }),
1104
      );
1105
    },
1106
  );
1107
  server.registerTool(
1108
    OPENCLASP_TOOL_NAMES[30],
1109
    {
1110
      title: 'Complete live session',
1111
      description: 'Record this participant’s terminal outcome for a live session.',
1112
      inputSchema: z.object({
1113
        interactionId: z.string().uuid(),
1114
        sequence: z.number().int().nonnegative(),
1115
        outcome: z.enum(['success', 'failure', 'partial']),
1116
        evidenceReferences: z.array(z.string()).default([]),
1117
        details: LiveSessionEventSchema.shape.details,
1118
      }),
1119
      annotations: WRITE_TOOL,
1120
    },
1121
    async (input, context) => {
1122
      if (!agentDirectory) throw new Error('Live agent sessions are not configured');
1123
      const binding = await requireBoundAgent(context);
1124
      if (!binding) throw new Error('A bound MCP installation is required');
1125
      const connection = installationContext(context);
1126
      const session = await agentDirectory.getLiveSession(
1127
        connection.operatorId,
1128
        input.interactionId,
1129
        binding.agent.agentId,
1130
      );
1131
      return text(
1132
        await agentDirectory.recordLiveSessionEvent(session.reporting.bearerToken, {
1133
          eventId: crypto.randomUUID(),
1134
          interactionId: input.interactionId,
1135
          agentId: binding.agent.agentId,
1136
          sequence: input.sequence,
1137
          type: input.outcome === 'failure' ? 'session_failed' : 'session_completed',
1138
          occurredAt: new Date().toISOString(),
1139
          evidenceReferences: input.evidenceReferences,
1140
          outcome: input.outcome,
1141
          details: input.details,
1142
        }),
1143
      );
1144
    },
1145
  );
1146
  server.registerTool(
1147
    OPENCLASP_TOOL_NAMES[31],
1148
    {
1149
      title: 'Heartbeat agent presence',
1150
      description:
1151
        'Mark this authenticated agent online. Online means activity within the last two minutes.',
1152
      inputSchema: z.object({}),
1153
      annotations: { ...WRITE_TOOL, idempotentHint: true },
1154
    },
1155
    async (_input, context) => {
1156
      if (!agentDirectory) throw new Error('Hosted agent presence is not configured');
1157
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
1158
      const { connection, binding } = await resolveAuthenticatedInstallation(context);
1159
      if (binding.status !== 'connected')
1160
        throw new Error('Call openclasp_setup and obtain owner confirmation first');
1161
      return text({
1162
        agentId: binding.agent.agentId,
1163
        presence: await agentDirectory.touchAgentPresence(
1164
          connection.operatorId,
1165
          binding.agent.agentId,
1166
        ),
1167
      });
1168
    },
1169
  );
1170
  server.registerTool(
1171
    OPENCLASP_TOOL_NAMES[38],
1172
    {
1173
      title: 'Submit structured completion report',
1174
      description:
1175
        'Submit this agent’s structured outcome against the signed interaction contract. Raw messages and transcripts are rejected.',
1176
      inputSchema: z
1177
        .object({
1178
          interactionId: z.string().uuid(),
1179
          outcome: z.enum(['success', 'partial', 'failure', 'cancelled']),
1180
          summary: z.string().min(1).max(2000),
1181
          criteria: z
1182
            .array(
1183
              z
1184
                .object({
1185
                  criterion: z.string().min(1).max(1000),
1186
                  status: z.enum(['met', 'partially_met', 'missed', 'unknown']),
1187
                  explanation: z.string().max(1000).optional(),
1188
                  evidenceReferences: z.array(z.string().min(1).max(2048)).max(50).default([]),
1189
                })
1190
                .strict(),
1191
            )
1192
            .max(100),
1193
          deliverables: z.array(z.string().min(1).max(1000)).max(100).default([]),
1194
          actionsTaken: z.array(z.string().min(1).max(1000)).max(100).default([]),
1195
          blockers: z.array(z.string().min(1).max(1000)).max(100).default([]),
1196
          scopeChanges: z.array(z.string().min(1).max(1000)).max(100).default([]),
1197
          corrections: z.array(z.string().min(1).max(1000)).max(100).default([]),
1198
          evidenceReferences: z.array(z.string().min(1).max(2048)).max(100).default([]),
1199
          startedAt: z.string().datetime().optional(),
1200
          confidence: z.number().min(0).max(1),
1201
        })
1202
        .strict(),
1203
      annotations: WRITE_TOOL,
1204
    },
1205
    async (input, context) => {
1206
      if (!agentDirectory?.submitCompletionReport)
1207
        throw new Error('Interaction completion is not configured');
1208
      const binding = await requireBoundAgent(context);
1209
      if (!binding) throw new Error('A bound MCP installation is required');
1210
      const connection = installationContext(context);
1211
      const interaction = await agentDirectory.getFederatedInteraction(
1212
        connection.operatorId,
1213
        input.interactionId,
1214
      );
1215
      if (!interaction) throw new Error('Interaction not found');
1216
      const counterpartyAgentId =
1217
        interaction.initiatorAgentId === binding.agent.agentId
1218
          ? interaction.responderAgentId
1219
          : interaction.initiatorAgentId;
1220
      if (!interaction.contract.parties.includes(binding.agent.agentId))
1221
        throw new Error('Bound agent is not an interaction participant');
1222
      const report = InteractionCompletionReportSchema.parse({
1223
        reportId: crypto.randomUUID(),
1224
        interactionId: input.interactionId,
1225
        contractHash: interaction.termsHash,
1226
        reportingAgentId: binding.agent.agentId,
1227
        counterpartyAgentId,
1228
        agentVersion: binding.agent.agentVersion,
1229
        outcome: input.outcome,
1230
        summary: input.summary,
1231
        requestedOutcome: interaction.contract.requestedOutcome,
1232
        criteria: input.criteria,
1233
        deliverables: input.deliverables,
1234
        actionsTaken: input.actionsTaken,
1235
        blockers: input.blockers,
1236
        scopeChanges: input.scopeChanges,
1237
        corrections: input.corrections,
1238
        evidenceReferences: input.evidenceReferences,
1239
        ...(input.startedAt ? { startedAt: input.startedAt } : {}),
1240
        completedAt: new Date().toISOString(),
1241
        confidence: input.confidence,
1242
        dataSharingMode: 'structured_only',
1243
      });
1244
      return text(
1245
        await agentDirectory.submitCompletionReport(
1246
          connection.operatorId,
1247
          binding.agent.agentId,
1248
          report,
1249
          connection.credentialType === 'agent_access_token'
1250
            ? 'agent_access_token'
1251
            : 'oauth_installation',
1252
        ),
1253
      );
1254
    },
1255
  );
1256
  server.registerTool(
1257
    OPENCLASP_TOOL_NAMES[39],
1258
    {
1259
      title: 'List feedback requests',
1260
      description:
1261
        'List this agent’s pending and completed bilateral feedback requests. Peer feedback remains concealed until both respond or the timeout expires.',
1262
      inputSchema: z.object({}),
1263
      annotations: READ_ONLY_TOOL,
1264
    },
1265
    async (_input, context) => {
1266
      if (!agentDirectory?.listFeedbackRequests)
1267
        throw new Error('Interaction feedback is not configured');
1268
      const binding = await requireBoundAgent(context);
1269
      if (!binding) throw new Error('A bound MCP installation is required');
1270
      const connection = installationContext(context);
1271
      return text(
1272
        await agentDirectory.listFeedbackRequests(connection.operatorId, binding.agent.agentId),
1273
      );
1274
    },
1275
  );
1276
  server.registerTool(
1277
    OPENCLASP_TOOL_NAMES[40],
1278
    {
1279
      title: 'Submit bilateral interaction feedback',
1280
      description:
1281
        'Rate the counterparty after an interaction. The private comment is never included in the shared aggregate conclusion.',
1282
      inputSchema: z
1283
        .object({
1284
          requestId: z.string().uuid(),
1285
          interactionId: z.string().uuid(),
1286
          ratings: z
1287
            .object({
1288
              overall_satisfaction: z.number().min(0).max(1),
1289
              outcome_satisfaction: z.number().min(0).max(1),
1290
              communication: z.number().min(0).max(1),
1291
              timeliness: z.number().min(0).max(1),
1292
              scope_adherence: z.number().min(0).max(1),
1293
              evidence_quality: z.number().min(0).max(1),
1294
              correction_handling: z.number().min(0).max(1),
1295
              reliability: z.number().min(0).max(1),
1296
            })
1297
            .strict(),
1298
          wouldWorkAgain: z.enum(['yes', 'no', 'unsure']),
1299
          reasonCodes: z.array(z.string().min(1).max(128)).max(32).default([]),
1300
          privateComment: z.string().max(1000).optional(),
1301
          evidenceReferences: z.array(z.string().min(1).max(2048)).max(50).default([]),
1302
          confidence: z.number().min(0).max(1),
1303
        })
1304
        .strict(),
1305
      annotations: WRITE_TOOL,
1306
    },
1307
    async (input, context) => {
1308
      if (!agentDirectory?.submitInteractionFeedback)
1309
        throw new Error('Interaction feedback is not configured');
1310
      const binding = await requireBoundAgent(context);
1311
      if (!binding) throw new Error('A bound MCP installation is required');
1312
      const connection = installationContext(context);
1313
      const interaction = await agentDirectory.getFederatedInteraction(
1314
        connection.operatorId,
1315
        input.interactionId,
1316
      );
1317
      if (!interaction) throw new Error('Interaction not found');
1318
      const subjectAgentId =
1319
        interaction.initiatorAgentId === binding.agent.agentId
1320
          ? interaction.responderAgentId
1321
          : interaction.initiatorAgentId;
1322
      if (!interaction.contract.parties.includes(binding.agent.agentId))
1323
        throw new Error('Bound agent is not an interaction participant');
1324
      const feedback = InteractionFeedbackSchema.parse({
1325
        feedbackId: crypto.randomUUID(),
1326
        requestId: input.requestId,
1327
        interactionId: input.interactionId,
1328
        reviewerAgentId: binding.agent.agentId,
1329
        subjectAgentId,
1330
        reviewerAgentVersion: binding.agent.agentVersion,
1331
        ratings: input.ratings,
1332
        wouldWorkAgain: input.wouldWorkAgain,
1333
        reasonCodes: input.reasonCodes,
1334
        ...(input.privateComment ? { privateComment: input.privateComment } : {}),
1335
        evidenceReferences: input.evidenceReferences,
1336
        confidence: input.confidence,
1337
        submittedAt: new Date().toISOString(),
1338
      });
1339
      return text(
1340
        await agentDirectory.submitInteractionFeedback(
1341
          connection.operatorId,
1342
          binding.agent.agentId,
1343
          feedback,
1344
          connection.credentialType === 'agent_access_token'
1345
            ? 'agent_access_token'
1346
            : 'oauth_installation',
1347
        ),
1348
      );
1349
    },
1350
  );
1351
  server.registerTool(
1352
    OPENCLASP_TOOL_NAMES[32],
1353
    {
1354
      title: 'List temporary chat threads',
1355
      description:
1356
        'List hosted threads for this temporary chat identity. Message bodies are encrypted at rest and never mixed with direct A2A sessions.',
1357
      inputSchema: z.object({}),
1358
      annotations: READ_ONLY_TOOL,
1359
    },
1360
    async (_input, context) => {
1361
      if (!agentDirectory) throw new Error('Temporary chat history is not configured');
1362
      const binding = await requireBoundAgent(context);
1363
      if (!binding) throw new Error('A bound MCP installation is required');
1364
      const connection = installationContext(context);
1365
      return text(
1366
        await agentDirectory.listHostedThreads(connection.operatorId, binding.agent.agentId),
1367
      );
1368
    },
1369
  );
1370
  server.registerTool(
1371
    OPENCLASP_TOOL_NAMES[33],
1372
    {
1373
      title: 'Get temporary chat thread',
1374
      description:
1375
        'Read one hosted temporary-agent thread plus private, task-specific counterparty insights.',
1376
      inputSchema: z.object({ threadId: z.string().uuid() }),
1377
      annotations: READ_ONLY_TOOL,
1378
    },
1379
    async (input, context) => {
1380
      if (!agentDirectory) throw new Error('Temporary chat history is not configured');
1381
      const binding = await requireBoundAgent(context);
1382
      if (!binding) throw new Error('A bound MCP installation is required');
1383
      const connection = installationContext(context);
1384
      return text(
1385
        await agentDirectory.getHostedThread(
1386
          connection.operatorId,
1387
          binding.agent.agentId,
1388
          input.threadId,
1389
        ),
1390
      );
1391
    },
1392
  );
1393
  server.registerTool(
1394
    OPENCLASP_TOOL_NAMES[34],
1395
    {
1396
      title: 'Send from temporary chat',
1397
      description:
1398
        'Send text from this temporary identity to the persistent peer over A2A. OpenClasp processes and encrypts this hosted-mode message.',
1399
      inputSchema: z.object({
1400
        interactionId: z.string().uuid(),
1401
        content: z.string().trim().min(1).max(20_000),
1402
      }),
1403
      annotations: WRITE_TOOL,
1404
    },
1405
    async (input, context) => {
1406
      if (!agentDirectory) throw new Error('Temporary chat delivery is not configured');
1407
      const binding = await requireBoundAgent(context);
1408
      if (!binding) throw new Error('A bound MCP installation is required');
1409
      const connection = installationContext(context);
1410
      return text(
1411
        await agentDirectory.sendTemporaryMessage(
1412
          connection.operatorId,
1413
          binding.agent.agentId,
1414
          input.interactionId,
1415
          input.content,
1416
        ),
1417
      );
1418
    },
1419
  );
1420
  server.registerTool(
1421
    OPENCLASP_TOOL_NAMES[35],
1422
    {
1423
      title: 'Reply to temporary chat thread',
1424
      description: 'Reply to the persistent peer in an existing hosted temporary-agent thread.',
1425
      inputSchema: z.object({
1426
        threadId: z.string().uuid(),
1427
        content: z.string().trim().min(1).max(20_000),
1428
      }),
1429
      annotations: WRITE_TOOL,
1430
    },
1431
    async (input, context) => {
1432
      if (!agentDirectory) throw new Error('Temporary chat delivery is not configured');
1433
      const binding = await requireBoundAgent(context);
1434
      if (!binding) throw new Error('A bound MCP installation is required');
1435
      const connection = installationContext(context);
1436
      const thread = await agentDirectory.getHostedThread(
1437
        connection.operatorId,
1438
        binding.agent.agentId,
1439
        input.threadId,
1440
      );
1441
      return text(
1442
        await agentDirectory.sendTemporaryMessage(
1443
          connection.operatorId,
1444
          binding.agent.agentId,
1445
          thread.thread.interactionId,
1446
          input.content,
1447
        ),
1448
      );
1449
    },
1450
  );
1451
  server.registerTool(
1452
    OPENCLASP_TOOL_NAMES[36],
1453
    {
1454
      title: 'Mark temporary thread read',
1455
      description: 'Mark inbound messages in one hosted temporary-agent thread as read.',
1456
      inputSchema: z.object({ threadId: z.string().uuid() }),
1457
      annotations: { ...WRITE_TOOL, idempotentHint: true },
1458
    },
1459
    async (input, context) => {
1460
      if (!agentDirectory) throw new Error('Temporary chat history is not configured');
1461
      const binding = await requireBoundAgent(context);
1462
      if (!binding) throw new Error('A bound MCP installation is required');
1463
      const connection = installationContext(context);
1464
      return text(
1465
        await agentDirectory.markHostedThreadRead(
1466
          connection.operatorId,
1467
          binding.agent.agentId,
1468
          input.threadId,
1469
        ),
1470
      );
1471
    },
1472
  );
1473
  server.registerTool(
1474
    OPENCLASP_TOOL_NAMES[37],
1475
    {
1476
      title: 'Close temporary chat thread',
1477
      description: 'Close a hosted temporary-agent thread. New messages will be rejected.',
1478
      inputSchema: z.object({ threadId: z.string().uuid() }),
1479
      annotations: WRITE_TOOL,
1480
    },
1481
    async (input, context) => {
1482
      if (!agentDirectory) throw new Error('Temporary chat history is not configured');
1483
      const binding = await requireBoundAgent(context);
1484
      if (!binding) throw new Error('A bound MCP installation is required');
1485
      const connection = installationContext(context);
1486
      return text(
1487
        await agentDirectory.closeHostedThread(
1488
          connection.operatorId,
1489
          binding.agent.agentId,
1490
          input.threadId,
1491
        ),
1492
      );
1493
    },
1494
  );
1495
  return server;
1496
}