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

Viewing files at commit 81afd7789b27Commit detailsView latest

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