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

Viewing files at commit 5b7999fcbb0cCommit detailsView latest

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