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

Viewing files at commit cd4450c423f7Commit detailsView latest

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