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

Viewing files at commit 5239911501a5Commit detailsView latest

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