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

Viewing files at commit 32cfcb5542e9Commit detailsView latest

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