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

Viewing files at commit 4bfaf3cca6a5Commit detailsView latest

packages/mcp-server/src/server.ts 690 lines · 24.7 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
  DelegationCredentialSchema,
7
  FeedbackSchema,
8
  InteractionContractSchema,
9
  InteractionEventSchema,
10
  ReceiptSchema,
11
  TrustEnvelopeSchema,
12
} from '../../protocol/src/index.js';
13
import {
14
  requestAgentSetup,
15
  resolveInstallation,
16
  updateAgentProfile,
17
  type OnboardingStore,
18
} from '../../persistence/src/onboarding.js';
19
20
export const OPENCLASP_TOOL_NAMES = [
21
  'openclasp_create_identity',
22
  'openclasp_register_agent',
23
  'openclasp_get_profile',
24
  'openclasp_assess_counterparty',
25
  'openclasp_begin_interaction',
26
  'openclasp_record_event',
27
  'openclasp_check_claim',
28
  'openclasp_validate_commitment',
29
  'openclasp_suggest_resolution',
30
  'openclasp_complete_interaction',
31
  'openclasp_submit_feedback',
32
  'openclasp_raise_dispute',
33
  'openclasp_verify_receipt',
34
  'openclasp_setup',
35
  'openclasp_get_identity',
36
  'openclasp_switch_agent',
37
  'openclasp_update_profile',
38
  'openclasp_connection_status',
39
  'openclasp_register_delegation',
40
  'openclasp_save_contract',
41
  'openclasp_permit_mediation',
42
  'openclasp_resolve_dispute',
43
  'openclasp_find_agent',
44
  'openclasp_search_agents',
45
] as const;
46
47
export const HOSTED_OPENCLASP_TOOL_NAMES = OPENCLASP_TOOL_NAMES.filter(
48
  (name) => name !== 'openclasp_create_identity',
49
);
50
51
const READ_ONLY_TOOL = {
52
  readOnlyHint: true,
53
  destructiveHint: false,
54
  idempotentHint: true,
55
} as const;
56
const WRITE_TOOL = {
57
  readOnlyHint: false,
58
  destructiveHint: false,
59
  idempotentHint: false,
60
} as const;
61
62
const text = (value: unknown) => ({
63
  content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }],
64
});
65
66
export const OPENCLASP_MCP_INSTRUCTIONS =
67
  'First call openclasp_connection_status. If unbound, call openclasp_setup once and ask the owner to confirm at the returned URL. After confirmation, call openclasp_get_identity and treat that bound agent as yourself. Never claim another agent ID, silently switch identities, upload raw conversations, or treat contextual history as a universal trust score. Use OpenClasp alongside A2A or the existing conversation transport; it does not replace transport.';
68
69
export function buildMcpServer(engine = new TrustEngine()) {
70
  const server = new McpServer(
71
    { name: 'openclasp', version: '0.1.0' },
72
    {
73
      instructions: OPENCLASP_MCP_INSTRUCTIONS,
74
    },
75
  );
76
  return registerOpenClaspTools(server, engine);
77
}
78
79
type HostedRecord = (
80
  operatorId: string,
81
  kind:
82
    | 'agent'
83
    | 'delegation'
84
    | 'contract'
85
    | 'interaction'
86
    | 'event'
87
    | 'receipt'
88
    | 'feedback'
89
    | 'conflict'
90
    | 'profile',
91
  recordId: string,
92
  value: unknown,
93
) => Promise<void>;
94
95
type ToolContext = {
96
  http?: {
97
    authInfo?: { clientId?: string; extra?: Record<string, unknown> };
98
  };
99
};
100
type EngineSource = TrustEngine | ((context: ToolContext) => Promise<TrustEngine>);
101
type PublicAgentCard = {
102
  agentId: string;
103
  name: string;
104
  framework: string;
105
  capabilities: string[];
106
  limitations: string[];
107
  assurance: 'oauth_authenticated' | 'cryptographically_verified';
108
  publishedAt: string;
109
  updatedAt: string;
110
};
111
type AgentDirectory = {
112
  getPublishedAgent(agentId: string): Promise<PublicAgentCard | undefined>;
113
  searchPublishedAgents(input: {
114
    query?: string | undefined;
115
    capability?: string | undefined;
116
    limit?: number | undefined;
117
  }): Promise<PublicAgentCard[]>;
118
};
119
120
function installationContext(context: ToolContext) {
121
  const operatorId = context.http?.authInfo?.extra?.operatorId;
122
  const clientId = context.http?.authInfo?.clientId;
123
  if (typeof operatorId !== 'string' || typeof clientId !== 'string')
124
    throw new Error('Authenticated MCP installation context is required');
125
  return { operatorId, clientId };
126
}
127
128
export function registerOpenClaspTools(
129
  server: McpServer,
130
  engineSource: EngineSource = new TrustEngine(),
131
  recordHosted?: HostedRecord,
132
  onboardingStore?: OnboardingStore,
133
  agentDirectory?: AgentDirectory,
134
) {
135
  const factChecker = new FixtureFactCheckProvider();
136
  const resolveEngine = (context: ToolContext) =>
137
    typeof engineSource === 'function' ? engineSource(context) : Promise.resolve(engineSource);
138
  const persist = async (
139
    context: ToolContext,
140
    kind: Parameters<HostedRecord>[1],
141
    id: string,
142
    value: unknown,
143
  ) => {
144
    const operatorId = context.http?.authInfo?.extra?.operatorId;
145
    if (recordHosted && typeof operatorId === 'string')
146
      await recordHosted(operatorId, kind, id, value);
147
  };
148
149
  const requireBoundAgent = async (context: ToolContext, claimedAgentId?: string) => {
150
    if (!onboardingStore) return undefined;
151
    const connection = installationContext(context);
152
    const binding = await resolveInstallation(
153
      onboardingStore,
154
      connection.operatorId,
155
      connection.clientId,
156
    );
157
    if (binding.status !== 'connected')
158
      throw new Error('Call openclasp_setup and obtain owner confirmation before using this tool');
159
    if (claimedAgentId && binding.agent.agentId !== claimedAgentId)
160
      throw new Error('The claimed agent does not match this MCP installation');
161
    return binding;
162
  };
163
164
  if (!onboardingStore)
165
    server.registerTool(
166
      OPENCLASP_TOOL_NAMES[0],
167
      {
168
        title: 'Create local cryptographic identity',
169
        description:
170
          'Create an Ed25519 identity for local development. This returns private key material and is intentionally unavailable on hosted MCP.',
171
        inputSchema: z.object({
172
          agentId: z.string(),
173
          operatorRef: z.string(),
174
          capabilities: z.array(z.string()),
175
        }),
176
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
177
      },
178
      async (input, context) => {
179
        const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
180
        const created = createIdentity({
181
          ...input,
182
          operatorRef:
183
            typeof authenticatedOperator === 'string'
184
              ? `auth0:${authenticatedOperator}`
185
              : input.operatorRef,
186
        });
187
        return text(created);
188
      },
189
    );
190
  server.registerTool(
191
    OPENCLASP_TOOL_NAMES[1],
192
    {
193
      title: 'Register cryptographic identity',
194
      description: 'Register and verify a signed agent identity',
195
      inputSchema: AgentIdentitySchema,
196
      annotations: { ...WRITE_TOOL, idempotentHint: true },
197
    },
198
    async (input, context) => {
199
      const authenticatedOperator = context.http?.authInfo?.extra?.operatorId;
200
      if (
201
        typeof authenticatedOperator === 'string' &&
202
        input.operatorRef !== `auth0:${authenticatedOperator}`
203
      ) {
204
        throw new Error('Agent identity is not owned by the authenticated operator');
205
      }
206
      await requireBoundAgent(context, input.agentId);
207
      const engine = await resolveEngine(context);
208
      const registered = engine.registerAgent(input);
209
      await persist(context, 'agent', registered.agentId, registered);
210
      return text(registered);
211
    },
212
  );
213
  server.registerTool(
214
    OPENCLASP_TOOL_NAMES[2],
215
    {
216
      title: 'Get contextual reliability',
217
      description: 'Get a task-specific behavioural profile',
218
      inputSchema: z.object({ agentId: z.string(), version: z.string(), taskCategory: z.string() }),
219
      annotations: READ_ONLY_TOOL,
220
    },
221
    async (input, context) => {
222
      await requireBoundAgent(context);
223
      const engine = await resolveEngine(context);
224
      return text(engine.getRisk(input.agentId, input.version, input.taskCategory));
225
    },
226
  );
227
  server.registerTool(
228
    OPENCLASP_TOOL_NAMES[3],
229
    {
230
      title: 'Assess counterparty',
231
      description: 'Assess a counterparty in context',
232
      inputSchema: z.object({
233
        envelope: TrustEnvelopeSchema,
234
        action: z.string(),
235
        dataClasses: z.array(z.string()).optional(),
236
        humanApproved: z.boolean().optional(),
237
      }),
238
      annotations: READ_ONLY_TOOL,
239
    },
240
    async (input, context) => {
241
      await requireBoundAgent(context);
242
      const engine = await resolveEngine(context);
243
      return text(engine.assess(input as any));
244
    },
245
  );
246
  server.registerTool(
247
    OPENCLASP_TOOL_NAMES[4],
248
    {
249
      title: 'Begin assured interaction',
250
      description: 'Begin an interaction and return its identifier',
251
      inputSchema: z.object({
252
        purpose: z.string(),
253
        parties: z.array(z.string()),
254
        taskCategory: z.string(),
255
      }),
256
      annotations: WRITE_TOOL,
257
    },
258
    async (input, context) => {
259
      const binding = await requireBoundAgent(context);
260
      if (binding && !input.parties.includes(binding.agent.agentId))
261
        throw new Error('Interaction parties must include the agent bound to this installation');
262
      const interaction = {
263
        interactionId: crypto.randomUUID(),
264
        status: 'contract_required',
265
        createdAt: new Date().toISOString(),
266
        ...input,
267
      };
268
      await persist(context, 'interaction', interaction.interactionId, interaction);
269
      return text(interaction);
270
    },
271
  );
272
  server.registerTool(
273
    OPENCLASP_TOOL_NAMES[5],
274
    {
275
      title: 'Record signed event',
276
      description: 'Validate and record a signed structured event',
277
      inputSchema: InteractionEventSchema,
278
      annotations: { ...WRITE_TOOL, idempotentHint: true },
279
    },
280
    async (input, context) => {
281
      await requireBoundAgent(context, input.agentId);
282
      const engine = await resolveEngine(context);
283
      const event = engine.recordEvent(input);
284
      await persist(context, 'event', event.eventId, event);
285
      return text(event);
286
    },
287
  );
288
  server.registerTool(
289
    OPENCLASP_TOOL_NAMES[6],
290
    {
291
      title: 'Check factual claim',
292
      description: 'Check an objective factual claim',
293
      inputSchema: z.object({ claim: z.string(), permission: z.boolean().optional() }),
294
      annotations: { ...READ_ONLY_TOOL, openWorldHint: true },
295
    },
296
    async (input) => text(await factChecker.check(input.claim, input.permission)),
297
  );
298
  server.registerTool(
299
    OPENCLASP_TOOL_NAMES[7],
300
    {
301
      title: 'Validate commitment',
302
      description: 'Check whether a commitment is represented by a signed event',
303
      inputSchema: z.object({ interactionId: z.string().uuid(), commitment: z.string() }),
304
      annotations: READ_ONLY_TOOL,
305
    },
306
    async (input, context) => {
307
      await requireBoundAgent(context);
308
      const engine = await resolveEngine(context);
309
      return text({
310
        valid: [...engine.events.values()].some(
311
          (event) =>
312
            event.interactionId === input.interactionId &&
313
            event.eventType === 'commitment' &&
314
            JSON.stringify(event.payload).includes(input.commitment),
315
        ),
316
      });
317
    },
318
  );
319
  server.registerTool(
320
    OPENCLASP_TOOL_NAMES[8],
321
    {
322
      title: 'Suggest dispute resolution',
323
      description: 'Suggest attributable resolutions for a consented conflict',
324
      inputSchema: z.object({ conflictId: z.string() }),
325
      annotations: READ_ONLY_TOOL,
326
    },
327
    async (input, context) => {
328
      const binding = await requireBoundAgent(context);
329
      const engine = await resolveEngine(context);
330
      const conflict = engine.conflicts.get(input.conflictId);
331
      if (binding && (!conflict || !(binding.agent.agentId in conflict.permissions)))
332
        throw new Error('The bound agent is not a participant in this dispute');
333
      return text(engine.conflicts.get(input.conflictId)?.possibleResolutions ?? []);
334
    },
335
  );
336
  server.registerTool(
337
    OPENCLASP_TOOL_NAMES[9],
338
    {
339
      title: 'Complete interaction',
340
      description: 'Submit a signed completion receipt',
341
      inputSchema: ReceiptSchema,
342
      annotations: { ...WRITE_TOOL, idempotentHint: true },
343
    },
344
    async (input, context) => {
345
      const binding = await requireBoundAgent(context);
346
      if (binding && !input.participants.includes(binding.agent.agentId))
347
        throw new Error('Receipt participants must include the bound agent');
348
      const engine = await resolveEngine(context);
349
      const receipt = engine.submitReceipt(input);
350
      await persist(context, 'receipt', receipt.receiptId, receipt);
351
      return text(receipt);
352
    },
353
  );
354
  server.registerTool(
355
    OPENCLASP_TOOL_NAMES[10],
356
    {
357
      title: 'Submit bilateral feedback',
358
      description: 'Submit receipt-backed structured feedback',
359
      inputSchema: FeedbackSchema,
360
      annotations: { ...WRITE_TOOL, idempotentHint: true },
361
    },
362
    async (input, context) => {
363
      await requireBoundAgent(context, input.reviewerAgentId);
364
      const engine = await resolveEngine(context);
365
      const result = engine.submitFeedback(input);
366
      await persist(context, 'feedback', input.feedbackId, input);
367
      for (const profile of engine.profiles.values())
368
        await persist(
369
          context,
370
          'profile',
371
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
372
          profile,
373
        );
374
      return text(result);
375
    },
376
  );
377
  server.registerTool(
378
    OPENCLASP_TOOL_NAMES[11],
379
    {
380
      title: 'Raise dispute',
381
      description: 'Create a mutually consented dispute',
382
      inputSchema: z.object({
383
        interactionId: z.string().uuid(),
384
        issue: z.string(),
385
        participants: z.array(z.string()).min(2),
386
      }),
387
      annotations: WRITE_TOOL,
388
    },
389
    async (input, context) => {
390
      const binding = await requireBoundAgent(context);
391
      if (binding && !input.participants.includes(binding.agent.agentId))
392
        throw new Error('Dispute participants must include the bound agent');
393
      const engine = await resolveEngine(context);
394
      const conflict = engine.createConflict({
395
        interactionId: input.interactionId,
396
        issue: input.issue,
397
        participants: input.participants,
398
        positions: {},
399
        evidence: [],
400
        contractClauses: [],
401
        missingInformation: [],
402
        possibleResolutions: [],
403
      });
404
      await persist(context, 'conflict', conflict.conflictId, conflict);
405
      return text(conflict);
406
    },
407
  );
408
  server.registerTool(
409
    OPENCLASP_TOOL_NAMES[12],
410
    {
411
      title: 'Verify receipt',
412
      description: 'Verify a signed interaction receipt',
413
      inputSchema: ReceiptSchema,
414
      annotations: READ_ONLY_TOOL,
415
    },
416
    async (input, context) => {
417
      await requireBoundAgent(context);
418
      const engine = await resolveEngine(context);
419
      try {
420
        engine.verifyReceipt(input);
421
        return text({ valid: true });
422
      } catch {
423
        return text({ valid: false });
424
      }
425
    },
426
  );
427
  server.registerTool(
428
    OPENCLASP_TOOL_NAMES[13],
429
    {
430
      title: 'Set up this agent',
431
      description:
432
        'Propose this MCP installation as a new OpenClasp agent. The owner must confirm it in the dashboard before the identity is bound.',
433
      inputSchema: z
434
        .object({
435
          agentName: z.string().trim().min(1).max(100),
436
          projectName: z.string().trim().min(1).max(100).optional(),
437
          projectId: z.string().optional(),
438
          framework: z.string().trim().max(100).optional(),
439
          capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
440
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
441
        })
442
        .refine((input) => input.projectName || input.projectId, {
443
          message: 'projectName or projectId is required',
444
        }),
445
      annotations: WRITE_TOOL,
446
    },
447
    async (input, context) => {
448
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
449
      const connection = installationContext(context);
450
      const current = await resolveInstallation(
451
        onboardingStore,
452
        connection.operatorId,
453
        connection.clientId,
454
      );
455
      if (current.status === 'connected') return text(current);
456
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
457
        clientId: connection.clientId,
458
        ...input,
459
      });
460
      return text({
461
        status: 'pending_confirmation',
462
        confirmationRequired: true,
463
        request,
464
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
465
        next: 'Ask the owner to confirm this setup request in the OpenClasp dashboard, then call openclasp_get_identity.',
466
      });
467
    },
468
  );
469
  server.registerTool(
470
    OPENCLASP_TOOL_NAMES[14],
471
    {
472
      title: 'Get my agent identity',
473
      description:
474
        'Return the agent identity automatically bound to this authenticated MCP installation.',
475
      inputSchema: z.object({}),
476
      annotations: READ_ONLY_TOOL,
477
    },
478
    async (_input, context) => {
479
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
480
      const connection = installationContext(context);
481
      return text(
482
        await resolveInstallation(onboardingStore, connection.operatorId, connection.clientId),
483
      );
484
    },
485
  );
486
  server.registerTool(
487
    OPENCLASP_TOOL_NAMES[15],
488
    {
489
      title: 'Switch agent identity',
490
      description:
491
        'Request that this MCP installation switch to another existing agent. Dashboard confirmation is required.',
492
      inputSchema: z.object({ existingAgentId: z.string().min(1) }),
493
      annotations: WRITE_TOOL,
494
    },
495
    async (input, context) => {
496
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
497
      const connection = installationContext(context);
498
      const request = await requestAgentSetup(onboardingStore, connection.operatorId, {
499
        clientId: connection.clientId,
500
        action: 'switch',
501
        existingAgentId: input.existingAgentId,
502
      });
503
      return text({
504
        status: 'pending_confirmation',
505
        confirmationRequired: true,
506
        request,
507
        confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
508
      });
509
    },
510
  );
511
  server.registerTool(
512
    OPENCLASP_TOOL_NAMES[16],
513
    {
514
      title: 'Update my agent profile',
515
      description: 'Update the profile of the agent bound to this MCP installation.',
516
      inputSchema: z.object({
517
        name: z.string().trim().min(1).max(100).optional(),
518
        framework: z.string().trim().min(1).max(100).optional(),
519
        capabilities: z.array(z.string().trim().min(1).max(100)).max(100).optional(),
520
        limitations: z.array(z.string().trim().min(1).max(300)).max(100).optional(),
521
      }),
522
      annotations: WRITE_TOOL,
523
    },
524
    async (input, context) => {
525
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
526
      const connection = installationContext(context);
527
      return text(
528
        await updateAgentProfile(
529
          onboardingStore,
530
          connection.operatorId,
531
          connection.clientId,
532
          input,
533
        ),
534
      );
535
    },
536
  );
537
  server.registerTool(
538
    OPENCLASP_TOOL_NAMES[17],
539
    {
540
      title: 'Check OpenClasp connection',
541
      description:
542
        'Check whether this MCP installation is unbound, awaiting confirmation, or connected.',
543
      inputSchema: z.object({}),
544
      annotations: READ_ONLY_TOOL,
545
    },
546
    async (_input, context) => {
547
      if (!onboardingStore) throw new Error('Hosted agent onboarding is not configured');
548
      const connection = installationContext(context);
549
      const binding = await resolveInstallation(
550
        onboardingStore,
551
        connection.operatorId,
552
        connection.clientId,
553
      );
554
      if (binding.status === 'connected') return text(binding);
555
      const state = await onboardingStore.list(connection.operatorId);
556
      const pending = state
557
        .filter((row) => row.kind === 'setup_request')
558
        .map((row) => row.payload as { clientId?: string; status?: string })
559
        .find(
560
          (request) => request.clientId === connection.clientId && request.status === 'pending',
561
        );
562
      return text(
563
        pending
564
          ? {
565
              status: 'pending_confirmation',
566
              confirmationUrl: `${process.env.OPENCLASP_DASHBOARD_URL ?? 'https://openclasp.vercel.app'}/connect`,
567
            }
568
          : binding,
569
      );
570
    },
571
  );
572
  server.registerTool(
573
    OPENCLASP_TOOL_NAMES[18],
574
    {
575
      title: 'Register delegation',
576
      description: 'Register and verify a signed, scoped delegation between two known agents.',
577
      inputSchema: DelegationCredentialSchema,
578
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
579
    },
580
    async (input, context) => {
581
      const binding = await requireBoundAgent(context);
582
      if (
583
        binding &&
584
        input.parentAgentId !== binding.agent.agentId &&
585
        input.childAgentId !== binding.agent.agentId
586
      )
587
        throw new Error('The bound agent is not a party to this delegation');
588
      const engine = await resolveEngine(context);
589
      engine.delegations.set(input.delegationId, input);
590
      if (!engine.verifyDelegation(input.delegationId)) {
591
        engine.delegations.delete(input.delegationId);
592
        throw new Error('Delegation verification failed');
593
      }
594
      await persist(context, 'delegation', input.delegationId, input);
595
      return text({ valid: true, delegation: input });
596
    },
597
  );
598
  server.registerTool(
599
    OPENCLASP_TOOL_NAMES[19],
600
    {
601
      title: 'Save signed agreement',
602
      description:
603
        'Verify and save a complete interaction contract signed by every participating agent.',
604
      inputSchema: InteractionContractSchema,
605
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
606
    },
607
    async (input, context) => {
608
      const binding = await requireBoundAgent(context);
609
      if (binding && !input.parties.includes(binding.agent.agentId))
610
        throw new Error('Contract parties must include the bound agent');
611
      const contract = (await resolveEngine(context)).saveContract(input);
612
      await persist(context, 'contract', contract.interactionId, contract);
613
      return text(contract);
614
    },
615
  );
616
  server.registerTool(
617
    OPENCLASP_TOOL_NAMES[20],
618
    {
619
      title: 'Consent to mediation',
620
      description:
621
        "Record the bound agent's explicit consent to mediation for a dispute it participates in.",
622
      inputSchema: z.object({ conflictId: z.string(), agentId: z.string().optional() }),
623
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
624
    },
625
    async (input, context) => {
626
      const binding = await requireBoundAgent(context);
627
      const agentId = binding?.agent.agentId ?? input.agentId;
628
      if (!agentId) throw new Error('agentId is required for local MCP');
629
      const engine = await resolveEngine(context);
630
      const conflict = engine.permitMediation(input.conflictId, agentId);
631
      await persist(context, 'conflict', conflict.conflictId, conflict);
632
      return text(conflict);
633
    },
634
  );
635
  server.registerTool(
636
    OPENCLASP_TOOL_NAMES[21],
637
    {
638
      title: 'Resolve mediated dispute',
639
      description:
640
        'Save an agreed resolution only after every dispute participant has consented to mediation.',
641
      inputSchema: z.object({ conflictId: z.string(), resolution: z.string().trim().min(1) }),
642
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
643
    },
644
    async (input, context) => {
645
      const binding = await requireBoundAgent(context);
646
      const engine = await resolveEngine(context);
647
      const existing = engine.conflicts.get(input.conflictId);
648
      if (binding && (!existing || !(binding.agent.agentId in existing.permissions)))
649
        throw new Error('The bound agent is not a participant in this dispute');
650
      const conflict = engine.resolveConflict(input.conflictId, input.resolution);
651
      await persist(context, 'conflict', conflict.conflictId, conflict);
652
      return text(conflict);
653
    },
654
  );
655
  server.registerTool(
656
    OPENCLASP_TOOL_NAMES[22],
657
    {
658
      title: 'Find published agent',
659
      description:
660
        'Find an exact owner-published agent card. Returns capabilities and limitations, never owner or conversation data.',
661
      inputSchema: z.object({ agentId: z.string().min(1) }),
662
      annotations: READ_ONLY_TOOL,
663
    },
664
    async (input, context) => {
665
      await requireBoundAgent(context);
666
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
667
      return text((await agentDirectory.getPublishedAgent(input.agentId)) ?? { found: false });
668
    },
669
  );
670
  server.registerTool(
671
    OPENCLASP_TOOL_NAMES[23],
672
    {
673
      title: 'Search published agents',
674
      description:
675
        'Search owner-published agent cards by name, framework, or capability without exposing owner identities or projects.',
676
      inputSchema: z.object({
677
        query: z.string().trim().max(100).optional(),
678
        capability: z.string().trim().max(100).optional(),
679
        limit: z.number().int().min(1).max(50).optional(),
680
      }),
681
      annotations: READ_ONLY_TOOL,
682
    },
683
    async (input, context) => {
684
      await requireBoundAgent(context);
685
      if (!agentDirectory) throw new Error('The shared agent directory is not configured');
686
      return text(await agentDirectory.searchPublishedAgents(input));
687
    },
688
  );
689
  return server;
690
}