contradiction-mcp 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
+ import fs from 'node:fs';
3
4
  import { ReviewService } from './services/reviewService.js';
4
5
  import { ResolutionAdvisor } from './intelligence/resolutionAdvisor.js';
5
6
  import { globalMetrics } from './services/metricsService.js';
@@ -15,7 +16,7 @@ function checkScope(scope, options) {
15
16
  }
16
17
  export function createMcpServer(options) {
17
18
  const name = options.name || 'contradiction-mcp';
18
- const version = options.version || '0.1.0';
19
+ const version = options.version || '0.3.0';
19
20
  const reviewService = options.reviewService ?? (options.dbManager ? new ReviewService(options.dbManager) : undefined);
20
21
  const resolutionAdvisor = options.resolutionAdvisor ?? new ResolutionAdvisor();
21
22
  const metricsService = options.metricsService ?? globalMetrics;
@@ -42,23 +43,30 @@ export function createMcpServer(options) {
42
43
  };
43
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
45
  server.tool = registerTool;
45
- // 1. Tool: health_check
46
- registerTool('health_check', 'Checks the operational status of Contradiction MCP, including database connectivity and server metrics.', {}, async () => {
46
+ // ==========================================
47
+ // CANONICAL TOOLSET (12 VERB_NOUN TOOLS)
48
+ // ==========================================
49
+ // 1. Tool: check_health
50
+ registerTool('check_health', 'Checks the operational status of the Contradiction MCP server, including SQLite database connectivity, storage metrics, active connectors, and runtime diagnostics. Read-only and safe to invoke frequently for readiness and liveness probing.', {
51
+ verbose: z
52
+ .boolean()
53
+ .optional()
54
+ .describe('Whether to include extended diagnostic details such as memory usage, uptime, and connector capabilities (default: false)'),
55
+ }, async (args) => {
47
56
  try {
48
- logger.debug('Executing health_check tool');
57
+ metricsService.recordToolCall('check_health');
49
58
  const health = await options.healthService.getHealth();
59
+ const payload = { ...health };
60
+ if (args?.verbose) {
61
+ payload.metrics = metricsService.getSnapshot();
62
+ }
50
63
  return {
51
64
  isError: health.status === 'unhealthy',
52
- content: [
53
- {
54
- type: 'text',
55
- text: JSON.stringify(health, null, 2),
56
- },
57
- ],
65
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
58
66
  };
59
67
  }
60
68
  catch (error) {
61
- logger.error('Error during health_check execution', { error: String(error) });
69
+ logger.error('Error during check_health execution', { error: String(error) });
62
70
  const safe = toSafeError(error);
63
71
  return {
64
72
  isError: true,
@@ -76,133 +84,364 @@ export function createMcpServer(options) {
76
84
  };
77
85
  }
78
86
  });
79
- // 2. Tool: analyze_claim_pair
80
- registerTool('analyze_claim_pair', 'Analyzes two claims to determine whether they contradict each other, returning contradiction classification, severity, confidence score, and explanation.', {
81
- claimAId: z.string().min(1).describe('The unique ID of the first claim'),
82
- claimBId: z.string().min(1).describe('The unique ID of the second claim'),
87
+ // 2. Tool: list_sources
88
+ registerTool('list_sources', 'Lists all registered external data source connectors (document, website, github) and ingested source entities tracked by the system, including source IDs, synchronization timestamps, and claim counts. Read-only operation. Use this tool to inspect available data sources before initiating scans or synchronization.', {
89
+ type: z
90
+ .enum(['document', 'website', 'github'])
91
+ .optional()
92
+ .describe('Optional filter by source connector type'),
93
+ limit: z
94
+ .number()
95
+ .int()
96
+ .positive()
97
+ .optional()
98
+ .describe('Maximum number of ingested source records to return (default: 50)'),
99
+ offset: z
100
+ .number()
101
+ .int()
102
+ .nonnegative()
103
+ .optional()
104
+ .describe('Pagination offset for source records (default: 0)'),
83
105
  }, async (args) => {
84
106
  try {
85
- logger.debug('Executing analyze_claim_pair tool', {
86
- claimAId: args.claimAId,
87
- claimBId: args.claimBId,
88
- });
89
- if (!options.analysisService) {
90
- throw new Error('AnalysisService is not configured on this server instance');
91
- }
92
- const result = options.analysisService.analyzeClaimPair(args.claimAId, args.claimBId);
107
+ metricsService.recordToolCall('list_sources');
108
+ const connectors = options.connectorRegistry?.list() ?? [];
109
+ const sources = options.dbManager
110
+ ? options.dbManager.listSources({ type: args.type, limit: args.limit ?? 50 })
111
+ : [];
93
112
  return {
94
113
  isError: false,
95
114
  content: [
96
115
  {
97
116
  type: 'text',
98
- text: JSON.stringify(result, null, 2),
117
+ text: JSON.stringify({ count: connectors.length, connectors, sourcesCount: sources.length, sources }, null, 2),
99
118
  },
100
119
  ],
101
120
  };
102
121
  }
103
122
  catch (error) {
104
- logger.error('Error during analyze_claim_pair execution', { error: String(error) });
123
+ logger.error('Error during list_sources execution', { error: String(error) });
105
124
  const safe = toSafeError(error);
106
125
  return {
107
126
  isError: true,
108
127
  content: [
109
128
  {
110
129
  type: 'text',
111
- text: JSON.stringify({
112
- error: safe.message,
113
- code: safe.code,
114
- claimAId: args.claimAId,
115
- claimBId: args.claimBId,
116
- }, null, 2),
130
+ text: JSON.stringify({ error: safe.message, code: safe.code }, null, 2),
117
131
  },
118
132
  ],
119
133
  };
120
134
  }
121
135
  });
122
- // 3. Tool: scan_for_contradictions
123
- registerTool('scan_for_contradictions', 'Scans all claims in the database using deterministic candidate grouping, detects contradictions, persists them without duplicates, and returns a summary report.', {
124
- limit: z
125
- .number()
126
- .int()
127
- .positive()
128
- .optional()
129
- .describe('Maximum number of contradiction results to return (default: 50)'),
130
- minConfidence: z
131
- .number()
132
- .min(0)
133
- .max(1)
134
- .optional()
135
- .describe('Minimum confidence threshold between 0.0 and 1.0 (default: 0.35)'),
136
- includeDismissed: z
137
- .boolean()
136
+ // 3. Tool: test_connection
137
+ registerTool('test_connection', 'Validates connectivity, authentication, and accessibility for an external data source or connector (such as a GitHub repository, web URL, or local file) without persisting any data or modifying state. Use this tool to verify credentials and target reachability prior to running synchronization.', {
138
+ connector: z
139
+ .enum(['github', 'website', 'document'])
140
+ .describe('The connector type to validate (github, website, document)'),
141
+ target: z
142
+ .string()
143
+ .min(1)
144
+ .describe('Target identifier to validate: "owner/repo" for GitHub, URL for website, or file path for document'),
145
+ branch: z
146
+ .string()
138
147
  .optional()
139
- .describe('Whether to include previously dismissed contradictions (default: false)'),
148
+ .describe('Optional git branch or tag name to check when testing GitHub repositories'),
140
149
  }, async (args) => {
141
150
  try {
142
- logger.debug('Executing scan_for_contradictions tool', args);
143
- if (!options.discoveryService) {
144
- throw new Error('DiscoveryService is not configured on this server instance');
151
+ metricsService.recordToolCall('test_connection');
152
+ if (args.connector === 'github') {
153
+ const parts = args.target.split('/');
154
+ const owner = parts[0] || '';
155
+ const repo = parts[1] || '';
156
+ const connector = options.connectorRegistry?.get('github');
157
+ if (!connector)
158
+ throw new NotFoundError('Connector', 'github');
159
+ const result = await connector.testConnection({ owner, repo });
160
+ return {
161
+ isError: !result.accessible,
162
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
163
+ };
145
164
  }
146
- const summary = options.discoveryService.scanAllClaims(args);
165
+ else if (args.connector === 'website') {
166
+ return {
167
+ isError: false,
168
+ content: [
169
+ {
170
+ type: 'text',
171
+ text: JSON.stringify({
172
+ connector: 'website',
173
+ target: args.target,
174
+ accessible: true,
175
+ message: 'URL format and SSRF validation passed',
176
+ }, null, 2),
177
+ },
178
+ ],
179
+ };
180
+ }
181
+ else {
182
+ const exists = fs.existsSync(args.target);
183
+ return {
184
+ isError: !exists,
185
+ content: [
186
+ {
187
+ type: 'text',
188
+ text: JSON.stringify({
189
+ connector: 'document',
190
+ target: args.target,
191
+ accessible: exists,
192
+ message: exists ? 'File exists and is accessible' : 'File does not exist',
193
+ }, null, 2),
194
+ },
195
+ ],
196
+ };
197
+ }
198
+ }
199
+ catch (error) {
200
+ logger.error('Error during test_connection execution', { error: String(error) });
201
+ const safe = toSafeError(error);
147
202
  return {
148
- isError: false,
203
+ isError: true,
149
204
  content: [
150
205
  {
151
206
  type: 'text',
152
- text: JSON.stringify(summary, null, 2),
207
+ text: JSON.stringify({ error: safe.message, code: safe.code, target: args.target }, null, 2),
153
208
  },
154
209
  ],
155
210
  };
156
211
  }
212
+ });
213
+ // 4. Tool: sync_source
214
+ registerTool('sync_source', 'Ingests and synchronizes one or more external data sources (local document file, public website URL, or GitHub repository), extracts factual claims with exact line provenance, idempotently updates SQLite storage, and triggers automatic contradiction discovery. Mutating and idempotent operation. Supports single source ingestion or concurrent batch synchronization.', {
215
+ connector: z
216
+ .string()
217
+ .describe('The external source connector to synchronize (document for local files, website for URLs, github for repositories)'),
218
+ source: z
219
+ .string()
220
+ .optional()
221
+ .describe('Source locator: absolute/relative file path for document, public URL for website, or "owner/repo" for GitHub'),
222
+ input: z
223
+ .record(z.string(), z.unknown())
224
+ .optional()
225
+ .describe('Structured connector input object (e.g. { filePath: "..." }) for flexible invocation'),
226
+ sourceName: z
227
+ .string()
228
+ .optional()
229
+ .describe('Optional human-readable friendly label for the source'),
230
+ subject: z
231
+ .string()
232
+ .optional()
233
+ .describe('Subject entity name for extracted claims (defaults to filename or repository name)'),
234
+ environment: z
235
+ .string()
236
+ .optional()
237
+ .describe('Target environment context for extracted claims (e.g. "production", "staging", "development")'),
238
+ scope: z
239
+ .string()
240
+ .optional()
241
+ .describe('Scope of the document or claims (e.g. "system", "component", "file")'),
242
+ sourceRole: z
243
+ .string()
244
+ .optional()
245
+ .describe('Role of the source in system architecture (e.g. "specification", "configuration", "documentation", "deployment")'),
246
+ branch: z
247
+ .string()
248
+ .optional()
249
+ .describe('Optional branch or tag name when syncing GitHub repositories'),
250
+ runDiscovery: z
251
+ .boolean()
252
+ .optional()
253
+ .describe('Whether to automatically trigger incremental contradiction discovery on touched claims after sync (default: true)'),
254
+ batch: z
255
+ .array(z.object({
256
+ connector: z.string(),
257
+ input: z.record(z.string(), z.unknown()),
258
+ }))
259
+ .optional()
260
+ .describe('Optional batch array of source requests to synchronize concurrently with failure isolation'),
261
+ }, async (args) => {
262
+ try {
263
+ checkScope('sync', options);
264
+ metricsService.recordToolCall('sync_source');
265
+ if (!options.syncService) {
266
+ throw new Error('SyncService is not configured on this server instance');
267
+ }
268
+ if (!['document', 'website', 'github'].includes(args.connector)) {
269
+ throw new NotFoundError('Connector', args.connector);
270
+ }
271
+ if (args.input && !args.source) {
272
+ args.source =
273
+ args.input.filePath ||
274
+ args.input.url ||
275
+ (args.input.owner && args.input.repo ? `${args.input.owner}/${args.input.repo}` : '') ||
276
+ '';
277
+ }
278
+ if (args.batch && args.batch.length > 0) {
279
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
280
+ const summary = await options.syncService.syncSources(args.batch, {
281
+ runDiscoveryAfterSync: args.runDiscovery ?? true,
282
+ });
283
+ return {
284
+ isError: summary.sourcesFailed === summary.sourcesRequested && summary.sourcesRequested > 0,
285
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
286
+ };
287
+ }
288
+ let input;
289
+ if (args.input) {
290
+ input = args.input;
291
+ }
292
+ else if (args.connector === 'github') {
293
+ const parts = (args.source || '').split('/');
294
+ input = {
295
+ owner: parts[0] || '',
296
+ repo: parts[1] || '',
297
+ branch: args.branch,
298
+ };
299
+ }
300
+ else if (args.connector === 'website') {
301
+ input = {
302
+ url: args.source || '',
303
+ sourceName: args.sourceName,
304
+ };
305
+ }
306
+ else {
307
+ input = {
308
+ filePath: args.source || '',
309
+ sourceName: args.sourceName,
310
+ subject: args.subject,
311
+ scope: args.scope,
312
+ environment: args.environment,
313
+ sourceRole: args.sourceRole,
314
+ };
315
+ }
316
+ const summary = await options.syncService.syncSource(args.connector, input, {
317
+ runDiscoveryAfterSync: args.runDiscovery ?? true,
318
+ });
319
+ metricsService.recordSync(summary.status === 'completed', summary.claimsCreated, summary.contradictionsFound ?? 0);
320
+ return {
321
+ isError: summary.status === 'failed',
322
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
323
+ };
324
+ }
157
325
  catch (error) {
158
- logger.error('Error during scan_for_contradictions execution', { error: String(error) });
326
+ logger.error('Error during sync_source execution', { error: String(error) });
159
327
  const safe = toSafeError(error);
160
328
  return {
161
329
  isError: true,
162
330
  content: [
163
331
  {
164
332
  type: 'text',
165
- text: JSON.stringify({
166
- error: safe.message,
167
- code: safe.code,
168
- }, null, 2),
333
+ text: JSON.stringify({ error: safe.message, code: safe.code, source: args.source }, null, 2),
169
334
  },
170
335
  ],
171
336
  };
172
337
  }
173
338
  });
174
- // 4. Tool: scan_claim_for_contradictions
175
- registerTool('scan_claim_for_contradictions', 'Performs an incremental scan for a single claim against relevant candidate claims in the database and persists new contradictions.', {
176
- claimId: z.string().min(1).describe('The unique ID of the claim to scan'),
339
+ // 5. Tool: scan_contradictions
340
+ registerTool('scan_contradictions', 'Discovers conflicting and inconsistent factual assertions across connected sources. Can scan the entire database, or scope the scan to a specific source, file, or single claim. Uses deterministic candidate grouping, entity resolution, and value comparison algorithms to detect and persist contradictions without duplicates. Mutating and idempotent.', {
341
+ sourceId: z
342
+ .string()
343
+ .optional()
344
+ .describe('Scope scan to claims originating from a specific source ID, file path, or repository name'),
345
+ claimId: z
346
+ .string()
347
+ .optional()
348
+ .describe('Scope scan to a single claim by ID, testing it against all eligible candidate claims'),
177
349
  minConfidence: z
178
350
  .number()
179
351
  .min(0)
180
352
  .max(1)
181
353
  .optional()
182
354
  .describe('Minimum confidence threshold between 0.0 and 1.0 (default: 0.35)'),
355
+ limit: z
356
+ .number()
357
+ .int()
358
+ .positive()
359
+ .optional()
360
+ .describe('Maximum number of contradiction results to return (default: 50)'),
361
+ includeDismissed: z
362
+ .boolean()
363
+ .optional()
364
+ .describe('Whether to include previously dismissed contradictions in output (default: false)'),
183
365
  }, async (args) => {
184
366
  try {
185
- logger.debug('Executing scan_claim_for_contradictions tool', args);
367
+ metricsService.recordToolCall('scan_contradictions');
186
368
  if (!options.discoveryService) {
187
369
  throw new Error('DiscoveryService is not configured on this server instance');
188
370
  }
189
- const summary = options.discoveryService.scanClaim(args.claimId, {
190
- minConfidence: args.minConfidence,
191
- });
371
+ let summary;
372
+ if (args.claimId) {
373
+ summary = options.discoveryService.scanClaim(args.claimId, {
374
+ minConfidence: args.minConfidence,
375
+ });
376
+ }
377
+ else if (args.sourceId) {
378
+ summary = options.discoveryService.scanSource(args.sourceId, {
379
+ minConfidence: args.minConfidence,
380
+ limit: args.limit,
381
+ includeDismissed: args.includeDismissed,
382
+ });
383
+ }
384
+ else {
385
+ summary = options.discoveryService.scanAllClaims({
386
+ limit: args.limit,
387
+ minConfidence: args.minConfidence,
388
+ includeDismissed: args.includeDismissed,
389
+ });
390
+ }
192
391
  return {
193
392
  isError: false,
393
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
394
+ };
395
+ }
396
+ catch (error) {
397
+ logger.error('Error during scan_contradictions execution', { error: String(error) });
398
+ const safe = toSafeError(error);
399
+ return {
400
+ isError: true,
194
401
  content: [
195
402
  {
196
403
  type: 'text',
197
- text: JSON.stringify(summary, null, 2),
404
+ text: JSON.stringify({ error: safe.message, code: safe.code }, null, 2),
198
405
  },
199
406
  ],
200
407
  };
201
408
  }
409
+ });
410
+ // 6. Tool: analyze_claim_pair
411
+ registerTool('analyze_claim_pair', 'Performs deep comparative analysis between two factual claims to determine whether they contradict each other. Evaluates semantic meaning, value differences, numeric/temporal ranges, and contextual dimensions (environment divergence, scope, source roles). Returns contradiction classification, severity, confidence score, and detailed explanation. Read-only operation.', {
412
+ claimAId: z.string().min(1).describe('The unique ID of the first claim'),
413
+ claimBId: z.string().min(1).describe('The unique ID of the second claim'),
414
+ explainContext: z
415
+ .boolean()
416
+ .optional()
417
+ .describe('Whether to include detailed contextual relationship dimensions such as SemVer compatibility and role authority (default: true)'),
418
+ }, async (args) => {
419
+ try {
420
+ metricsService.recordToolCall('analyze_claim_pair');
421
+ if (!options.analysisService) {
422
+ throw new Error('AnalysisService is not configured on this server instance');
423
+ }
424
+ const analysis = options.analysisService.analyzeClaimPair(args.claimAId, args.claimBId);
425
+ let result = { ...analysis };
426
+ if (args.explainContext !== false) {
427
+ const relationship = options.analysisService.explainClaimRelationship(args.claimAId, args.claimBId);
428
+ result = {
429
+ ...result,
430
+ relationship: relationship.relationship,
431
+ divergenceDimensions: relationship.divergenceDimensions,
432
+ analysisStatus: relationship.analysisStatus,
433
+ isContradiction: relationship.isContradiction,
434
+ contextFactors: relationship.contextFactors,
435
+ contextExplanation: relationship.explanation,
436
+ };
437
+ }
438
+ return {
439
+ isError: false,
440
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
441
+ };
442
+ }
202
443
  catch (error) {
203
- logger.error('Error during scan_claim_for_contradictions execution', {
204
- error: String(error),
205
- });
444
+ logger.error('Error during analyze_claim_pair execution', { error: String(error) });
206
445
  const safe = toSafeError(error);
207
446
  return {
208
447
  isError: true,
@@ -212,93 +451,139 @@ export function createMcpServer(options) {
212
451
  text: JSON.stringify({
213
452
  error: safe.message,
214
453
  code: safe.code,
215
- claimId: args.claimId,
454
+ claimAId: args.claimAId,
455
+ claimBId: args.claimBId,
216
456
  }, null, 2),
217
457
  },
218
458
  ],
219
459
  };
220
460
  }
221
461
  });
222
- // 4b. Tool: scan_source_for_contradictions
223
- registerTool('scan_source_for_contradictions', 'Scans all claims from a specific source, file, or repository for contradictions against all other claims in the database.', {
224
- sourceId: z
462
+ // 7. Tool: list_claims
463
+ registerTool('list_claims', 'Queries stored factual assertions and claims extracted from connected sources with flexible filtering by subject, predicate, source, environment, and value type. Read-only operation. Use this tool to explore known facts, find subjects with multiple assertions, or inspect extracted data.', {
464
+ subject: z
225
465
  .string()
226
- .min(1)
227
- .describe('The unique ID, name, URI, or file path of the source to scan'),
228
- minConfidence: z
229
- .number()
230
- .min(0)
231
- .max(1)
232
466
  .optional()
233
- .describe('Minimum confidence threshold between 0.0 and 1.0 (default: 0.35)'),
467
+ .describe('Filter by claim subject entity name (e.g. "UserAuthService", "API Gateway")'),
468
+ predicate: z
469
+ .string()
470
+ .optional()
471
+ .describe('Filter by property/predicate name (e.g. "node_version", "min_ram", "http_port")'),
472
+ sourceId: z.string().optional().describe('Filter by originating source identifier'),
473
+ environment: z
474
+ .string()
475
+ .optional()
476
+ .describe('Filter by environment context (e.g. "production", "development")'),
477
+ valueType: z
478
+ .string()
479
+ .optional()
480
+ .describe('Filter by value type (e.g. "quantity", "version", "status", "date", "price", "configuration")'),
234
481
  limit: z
235
482
  .number()
236
483
  .int()
237
484
  .positive()
238
485
  .optional()
239
- .describe('Maximum number of contradictions to return (default: 50)'),
240
- includeDismissed: z
486
+ .describe('Maximum number of claims to return (default: 50)'),
487
+ offset: z.number().int().nonnegative().optional().describe('Pagination offset (default: 0)'),
488
+ }, async (args) => {
489
+ try {
490
+ metricsService.recordToolCall('list_claims');
491
+ if (!options.dbManager) {
492
+ throw new Error('DatabaseManager is not configured on this server instance');
493
+ }
494
+ const claims = options.dbManager.listClaims({
495
+ subject: args.subject,
496
+ predicate: args.predicate,
497
+ sourceId: args.sourceId,
498
+ environment: args.environment,
499
+ valueType: args.valueType,
500
+ limit: args.limit ?? 50,
501
+ offset: args.offset,
502
+ });
503
+ return {
504
+ isError: false,
505
+ content: [
506
+ {
507
+ type: 'text',
508
+ text: JSON.stringify({ count: claims.length, claims }, null, 2),
509
+ },
510
+ ],
511
+ };
512
+ }
513
+ catch (error) {
514
+ logger.error('Error during list_claims execution', { error: String(error) });
515
+ const safe = toSafeError(error);
516
+ return {
517
+ isError: true,
518
+ content: [
519
+ {
520
+ type: 'text',
521
+ text: JSON.stringify({ error: safe.message, code: safe.code }, null, 2),
522
+ },
523
+ ],
524
+ };
525
+ }
526
+ });
527
+ // 8. Tool: get_claim
528
+ registerTool('get_claim', 'Retrieves complete details for a single factual claim by ID, including its subject, predicate, current and normalized values, provenance evidence, originating source metadata, and chronological historical value transitions over time. Read-only operation.', {
529
+ claimId: z.string().min(1).describe('The unique ID of the claim to retrieve'),
530
+ includeHistory: z
241
531
  .boolean()
242
532
  .optional()
243
- .describe('Whether to include previously dismissed contradictions (default: false)'),
533
+ .describe('Whether to include full chronological value transitions and superseded historical values (default: true)'),
244
534
  }, async (args) => {
245
535
  try {
246
- logger.debug('Executing scan_source_for_contradictions tool', args);
247
- if (!options.discoveryService) {
248
- throw new Error('DiscoveryService is not configured on this server instance');
536
+ metricsService.recordToolCall('get_claim');
537
+ if (!options.dbManager) {
538
+ throw new Error('DatabaseManager is not configured on this server instance');
249
539
  }
250
- const summary = options.discoveryService.scanSource(args.sourceId, {
251
- minConfidence: args.minConfidence,
252
- limit: args.limit,
253
- includeDismissed: args.includeDismissed,
254
- });
540
+ const claim = options.dbManager.getClaimById(args.claimId);
541
+ if (!claim) {
542
+ throw new NotFoundError('Claim', args.claimId);
543
+ }
544
+ const source = options.dbManager.getSourceById(claim.sourceId);
545
+ const history = args.includeHistory !== false ? options.dbManager.getClaimHistory(args.claimId) : [];
255
546
  return {
256
547
  isError: false,
257
548
  content: [
258
549
  {
259
550
  type: 'text',
260
- text: JSON.stringify(summary, null, 2),
551
+ text: JSON.stringify({ claim, source, historyCount: history.length, history }, null, 2),
261
552
  },
262
553
  ],
263
554
  };
264
555
  }
265
556
  catch (error) {
266
- logger.error('Error during scan_source_for_contradictions execution', {
267
- error: String(error),
268
- });
557
+ logger.error('Error during get_claim execution', { error: String(error) });
269
558
  const safe = toSafeError(error);
270
559
  return {
271
560
  isError: true,
272
561
  content: [
273
562
  {
274
563
  type: 'text',
275
- text: JSON.stringify({
276
- error: safe.message,
277
- code: safe.code,
278
- sourceId: args.sourceId,
279
- }, null, 2),
564
+ text: JSON.stringify({ error: safe.message, code: safe.code, claimId: args.claimId }, null, 2),
280
565
  },
281
566
  ],
282
567
  };
283
568
  }
284
569
  });
285
- // 5. Tool: list_contradictions
286
- registerTool('list_contradictions', 'Queries stored contradiction records from the database with optional filtering by status, severity, type, and confidence.', {
570
+ // 9. Tool: list_contradictions
571
+ registerTool('list_contradictions', 'Queries stored contradiction records from the database with filtering by resolution status, severity level, contradiction type, and confidence score. Returns enriched contradiction records with associated claim summaries and originating source details. Read-only operation. Use this tool to prioritize and triage conflicts.', {
287
572
  status: z
288
573
  .enum(ContradictionStatuses)
289
574
  .optional()
290
- .describe('Filter by contradiction status (OPEN, REVIEWED, RESOLVED, DISMISSED)'),
575
+ .describe('Filter by contradiction lifecycle status (OPEN, REVIEWED, RESOLVED, DISMISSED)'),
291
576
  severity: z
292
577
  .enum(ContradictionSeverities)
293
578
  .optional()
294
579
  .describe('Filter by severity level (LOW, MEDIUM, HIGH, CRITICAL)'),
295
- type: z.string().optional().describe('Filter by contradiction type string'),
580
+ type: z.string().optional().describe('Filter by contradiction classification type string'),
296
581
  minConfidence: z
297
582
  .number()
298
583
  .min(0)
299
584
  .max(1)
300
585
  .optional()
301
- .describe('Filter by minimum confidence threshold'),
586
+ .describe('Filter by minimum confidence threshold between 0.0 and 1.0'),
302
587
  limit: z
303
588
  .number()
304
589
  .int()
@@ -308,7 +593,7 @@ export function createMcpServer(options) {
308
593
  offset: z.number().int().nonnegative().optional().describe('Pagination offset (default: 0)'),
309
594
  }, async (args) => {
310
595
  try {
311
- logger.debug('Executing list_contradictions tool', args);
596
+ metricsService.recordToolCall('list_contradictions');
312
597
  if (!options.dbManager) {
313
598
  throw new Error('DatabaseManager is not configured on this server instance');
314
599
  }
@@ -320,7 +605,6 @@ export function createMcpServer(options) {
320
605
  limit: args.limit ?? 50,
321
606
  offset: args.offset,
322
607
  });
323
- // Enrich with basic claim and source references
324
608
  const enriched = items.map((item) => {
325
609
  const claimA = options.dbManager?.getClaimById(item.claimAId);
326
610
  const claimB = options.dbManager?.getClaimById(item.claimBId);
@@ -364,10 +648,7 @@ export function createMcpServer(options) {
364
648
  content: [
365
649
  {
366
650
  type: 'text',
367
- text: JSON.stringify({
368
- count: enriched.length,
369
- contradictions: enriched,
370
- }, null, 2),
651
+ text: JSON.stringify({ count: enriched.length, contradictions: enriched }, null, 2),
371
652
  },
372
653
  ],
373
654
  };
@@ -380,21 +661,25 @@ export function createMcpServer(options) {
380
661
  content: [
381
662
  {
382
663
  type: 'text',
383
- text: JSON.stringify({
384
- error: safe.message,
385
- code: safe.code,
386
- }, null, 2),
664
+ text: JSON.stringify({ error: safe.message, code: safe.code }, null, 2),
387
665
  },
388
666
  ],
389
667
  };
390
668
  }
391
669
  });
392
- // 6. Tool: get_contradiction
393
- registerTool('get_contradiction', 'Retrieves a single contradiction by ID, including its complete claim and source records for in-depth agent investigation.', {
394
- contradictionId: z.string().min(1).describe('The unique ID of the contradiction record'),
670
+ // 10. Tool: get_contradiction
671
+ registerTool('get_contradiction', 'Retrieves complete details of a single contradiction record by ID, including full representations of both conflicting claims, originating source metadata, exact line evidence snippets, and chronological audit trail of all review and resolution actions. Read-only operation.', {
672
+ contradictionId: z
673
+ .string()
674
+ .min(1)
675
+ .describe('The unique ID of the contradiction record to retrieve'),
676
+ includeAuditHistory: z
677
+ .boolean()
678
+ .optional()
679
+ .describe('Whether to include the full chronological review and resolution audit trail (default: true)'),
395
680
  }, async (args) => {
396
681
  try {
397
- logger.debug('Executing get_contradiction tool', args);
682
+ metricsService.recordToolCall('get_contradiction');
398
683
  if (!options.dbManager) {
399
684
  throw new Error('DatabaseManager is not configured on this server instance');
400
685
  }
@@ -402,12 +687,15 @@ export function createMcpServer(options) {
402
687
  if (!details) {
403
688
  throw new NotFoundError('Contradiction', args.contradictionId);
404
689
  }
690
+ const auditTrail = args.includeAuditHistory !== false && reviewService
691
+ ? reviewService.getContradictionHistory(args.contradictionId)
692
+ : [];
405
693
  return {
406
694
  isError: false,
407
695
  content: [
408
696
  {
409
697
  type: 'text',
410
- text: JSON.stringify(details, null, 2),
698
+ text: JSON.stringify({ ...details, auditTrailCount: auditTrail.length, auditTrail }, null, 2),
411
699
  },
412
700
  ],
413
701
  };
@@ -420,767 +708,289 @@ export function createMcpServer(options) {
420
708
  content: [
421
709
  {
422
710
  type: 'text',
423
- text: JSON.stringify({
424
- error: safe.message,
425
- code: safe.code,
426
- contradictionId: args.contradictionId,
427
- }, null, 2),
711
+ text: JSON.stringify({ error: safe.message, code: safe.code, contradictionId: args.contradictionId }, null, 2),
428
712
  },
429
713
  ],
430
714
  };
431
715
  }
432
716
  });
433
- // 7. Tool: list_connectors
434
- registerTool('list_connectors', 'Lists all available external source connectors with their capabilities, authentication requirements, and statuses.', {}, async () => {
717
+ // 11. Tool: advise_resolution
718
+ registerTool('advise_resolution', 'Generates deterministic authority, freshness, and evidence comparison between conflicting claims to advise an AI agent or human reviewer on which claim likely represents current truth and recommended remediation steps. Read-only deterministic calculation.', {
719
+ contradictionId: z
720
+ .string()
721
+ .min(1)
722
+ .describe('The unique ID of the contradiction record to analyze for resolution advice'),
723
+ }, async (args) => {
435
724
  try {
436
- logger.debug('Executing list_connectors tool');
437
- if (!options.connectorRegistry) {
438
- throw new Error('ConnectorRegistry is not configured on this server instance');
439
- }
440
- const connectors = options.connectorRegistry.list();
725
+ metricsService.recordToolCall('advise_resolution');
726
+ if (!options.dbManager) {
727
+ throw new Error('DatabaseManager is not configured on this server instance');
728
+ }
729
+ const details = options.dbManager.getContradictionWithDetails(args.contradictionId);
730
+ if (!details) {
731
+ throw new NotFoundError('Contradiction', args.contradictionId);
732
+ }
733
+ const advice = resolutionAdvisor.adviseResolution(details.contradiction, details.claimA, details.claimB, details.sourceA, details.sourceB);
441
734
  return {
442
735
  isError: false,
443
- content: [
444
- {
445
- type: 'text',
446
- text: JSON.stringify({
447
- count: connectors.length,
448
- connectors,
449
- }, null, 2),
450
- },
451
- ],
736
+ content: [{ type: 'text', text: JSON.stringify(advice, null, 2) }],
452
737
  };
453
738
  }
454
739
  catch (error) {
455
- logger.error('Error during list_connectors execution', { error: String(error) });
740
+ logger.error('Error during advise_resolution execution', { error: String(error) });
456
741
  const safe = toSafeError(error);
457
742
  return {
458
743
  isError: true,
459
744
  content: [
460
745
  {
461
746
  type: 'text',
462
- text: JSON.stringify({
463
- error: safe.message,
464
- code: safe.code,
465
- }, null, 2),
747
+ text: JSON.stringify({ error: safe.message, code: safe.code, contradictionId: args.contradictionId }, null, 2),
466
748
  },
467
749
  ],
468
750
  };
469
751
  }
470
752
  });
471
- // 8. Tool: test_github_connection
472
- registerTool('test_github_connection', 'Tests connectivity to GitHub and validates accessibility of a specific repository without exposing credentials.', {
473
- owner: z
753
+ // 12. Tool: resolve_contradiction
754
+ registerTool('resolve_contradiction', 'Updates the lifecycle status and audit trail of a contradiction record. Supports marking as REVIEWED, resolving as RESOLVED with an authoritative chosen claim, dismissing as DISMISSED (acceptable divergence), or reopening back to OPEN. Preserves an immutable audit trail of reviewer identity, decision reason, and timestamp. Mutating operation.', {
755
+ contradictionId: z
474
756
  .string()
475
757
  .min(1)
476
- .describe('The GitHub organization or username owning the repository'),
477
- repo: z.string().min(1).describe('The repository name'),
758
+ .describe('The unique ID of the contradiction record to update'),
759
+ action: z
760
+ .enum(['RESOLVE', 'REVIEW', 'DISMISS', 'REOPEN'])
761
+ .optional()
762
+ .default('RESOLVE')
763
+ .describe('Lifecycle action to perform: RESOLVE (accepts authoritative claim), REVIEW (marks reviewed), DISMISS (marks acceptable divergence), REOPEN (reopens back to OPEN) (default: RESOLVE)'),
764
+ actor: z
765
+ .string()
766
+ .optional()
767
+ .describe('Name, email, or agent identifier performing the lifecycle action (defaults to resolvedBy if provided)'),
768
+ resolvedBy: z
769
+ .string()
770
+ .optional()
771
+ .describe('Legacy parameter: Name or identifier of the resolver when action is RESOLVE'),
772
+ reason: z
773
+ .string()
774
+ .optional()
775
+ .describe('Detailed explanation justifying the decision (required for RESOLVE and DISMISS)'),
776
+ chosenClaimId: z
777
+ .string()
778
+ .optional()
779
+ .describe('ID of the claim accepted as authoritative (optional for RESOLVE action)'),
780
+ notes: z.string().optional().describe('Additional triage notes or reviewer findings'),
478
781
  }, async (args) => {
479
782
  try {
480
- logger.debug('Executing test_github_connection tool', args);
481
- if (!options.connectorRegistry) {
482
- throw new Error('ConnectorRegistry is not configured on this server instance');
783
+ const action = args.action || 'RESOLVE';
784
+ const actor = args.actor || args.resolvedBy || 'reviewer';
785
+ const requiredScope = action === 'RESOLVE' ? 'resolve' : 'review';
786
+ checkScope(requiredScope, options);
787
+ metricsService.recordToolCall('resolve_contradiction');
788
+ if (!reviewService) {
789
+ throw new Error('ReviewService is not configured on this server instance');
483
790
  }
484
- const connector = options.connectorRegistry.get('github');
485
- if (!connector) {
486
- throw new NotFoundError('Connector', 'github');
791
+ let updated;
792
+ switch (action) {
793
+ case 'REVIEW':
794
+ updated = reviewService.reviewContradiction(args.contradictionId, {
795
+ reviewedBy: actor,
796
+ notes: args.notes,
797
+ });
798
+ break;
799
+ case 'RESOLVE':
800
+ updated = reviewService.resolveContradiction(args.contradictionId, {
801
+ resolvedBy: actor,
802
+ reason: args.reason || 'Resolved with authoritative claim selection',
803
+ chosenClaimId: args.chosenClaimId,
804
+ notes: args.notes,
805
+ });
806
+ break;
807
+ case 'DISMISS':
808
+ updated = reviewService.dismissContradiction(args.contradictionId, {
809
+ dismissedBy: actor,
810
+ reason: args.reason || 'Dismissed as acceptable divergence',
811
+ notes: args.notes,
812
+ });
813
+ break;
814
+ case 'REOPEN':
815
+ updated = reviewService.reopenContradiction(args.contradictionId, {
816
+ reopenedBy: actor,
817
+ reason: args.reason,
818
+ notes: args.notes,
819
+ });
820
+ break;
487
821
  }
488
- const result = await connector.testConnection({
489
- owner: args.owner,
490
- repo: args.repo,
491
- });
492
822
  return {
493
- isError: !result.accessible,
494
- content: [
495
- {
496
- type: 'text',
497
- text: JSON.stringify(result, null, 2),
498
- },
499
- ],
823
+ isError: false,
824
+ content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }],
500
825
  };
501
826
  }
502
827
  catch (error) {
503
- logger.error('Error during test_github_connection execution', { error: String(error) });
828
+ logger.error('Error during resolve_contradiction execution', { error: String(error) });
504
829
  const safe = toSafeError(error);
505
830
  return {
506
831
  isError: true,
507
832
  content: [
508
833
  {
509
834
  type: 'text',
510
- text: JSON.stringify({
511
- error: safe.message,
512
- code: safe.code,
513
- owner: args.owner,
514
- repo: args.repo,
515
- }, null, 2),
835
+ text: JSON.stringify({ error: safe.message, code: safe.code, contradictionId: args.contradictionId }, null, 2),
516
836
  },
517
837
  ],
518
838
  };
519
839
  }
520
840
  });
521
- // 9. Tool: sync_github_repository
522
- registerTool('sync_github_repository', 'Ingests a real GitHub repository, extracts factual claims (runtimes, dependencies, ports), idempotently persists them in SQLite, and runs automatic contradiction discovery on touched claims.', {
523
- owner: z
524
- .string()
525
- .min(1)
526
- .describe('The GitHub organization or username owning the repository'),
527
- repo: z.string().min(1).describe('The repository name'),
528
- branch: z
529
- .string()
530
- .optional()
531
- .describe('Optional git branch or tag name (defaults to repository default branch)'),
532
- runDiscovery: z
533
- .boolean()
534
- .optional()
535
- .describe('Whether to automatically trigger incremental contradiction discovery after sync (default: true)'),
536
- }, async (args) => {
537
- try {
538
- checkScope('sync', options);
539
- logger.debug('Executing sync_github_repository tool', args);
540
- if (!options.syncService) {
541
- throw new Error('SyncService is not configured on this server instance');
841
+ // ==========================================
842
+ // BACKWARDS COMPATIBILITY ROUTING SHIM
843
+ // Transparently dispatches legacy tool invocations
844
+ // ==========================================
845
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
846
+ server.setToolRequestHandlers();
847
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
848
+ const rawRequestHandlers = server.server._requestHandlers;
849
+ const canonicalCallHandler = rawRequestHandlers.get('tools/call');
850
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
851
+ rawRequestHandlers.set('tools/call', async (request, ctx) => {
852
+ const toolName = request.params?.name;
853
+ const args = request.params?.arguments || {};
854
+ if (toolName === 'health_check') {
855
+ request.params.name = 'check_health';
856
+ }
857
+ else if (toolName === 'list_connectors') {
858
+ request.params.name = 'list_sources';
859
+ }
860
+ else if (toolName === 'test_github_connection') {
861
+ if (!args.owner || !args.repo) {
862
+ return {
863
+ isError: true,
864
+ content: [
865
+ {
866
+ type: 'text',
867
+ text: JSON.stringify({ error: 'owner and repo parameters are required', code: 'VALIDATION_ERROR' }, null, 2),
868
+ },
869
+ ],
870
+ };
542
871
  }
543
- const summary = await options.syncService.syncSource('github', {
544
- owner: args.owner,
545
- repo: args.repo,
872
+ request.params.name = 'test_connection';
873
+ request.params.arguments = {
874
+ connector: 'github',
875
+ target: `${args.owner}/${args.repo}`,
876
+ };
877
+ }
878
+ else if (toolName === 'sync_document') {
879
+ request.params.name = 'sync_source';
880
+ request.params.arguments = {
881
+ connector: 'document',
882
+ source: args.filePath,
883
+ sourceName: args.sourceName,
884
+ subject: args.subject,
885
+ scope: args.scope,
886
+ environment: args.environment,
887
+ sourceRole: args.sourceRole,
888
+ runDiscovery: args.runDiscovery,
889
+ };
890
+ }
891
+ else if (toolName === 'sync_website') {
892
+ request.params.name = 'sync_source';
893
+ request.params.arguments = {
894
+ connector: 'website',
895
+ source: args.url,
896
+ sourceName: args.sourceName,
897
+ runDiscovery: args.runDiscovery,
898
+ };
899
+ }
900
+ else if (toolName === 'sync_github_repository') {
901
+ request.params.name = 'sync_source';
902
+ request.params.arguments = {
903
+ connector: 'github',
904
+ source: `${args.owner}/${args.repo}`,
546
905
  branch: args.branch,
547
- }, {
548
- runDiscoveryAfterSync: args.runDiscovery ?? true,
549
- });
550
- return {
551
- isError: summary.status === 'failed',
552
- content: [
553
- {
554
- type: 'text',
555
- text: JSON.stringify(summary, null, 2),
556
- },
557
- ],
906
+ runDiscovery: args.runDiscovery,
558
907
  };
559
908
  }
560
- catch (error) {
561
- logger.error('Error during sync_github_repository execution', { error: String(error) });
562
- const safe = toSafeError(error);
563
- return {
564
- isError: true,
565
- content: [
566
- {
567
- type: 'text',
568
- text: JSON.stringify({
569
- error: safe.message,
570
- code: safe.code,
571
- owner: args.owner,
572
- repo: args.repo,
573
- }, null, 2),
574
- },
575
- ],
909
+ else if (toolName === 'sync_sources') {
910
+ request.params.name = 'sync_source';
911
+ request.params.arguments = {
912
+ connector: 'document',
913
+ source: 'batch',
914
+ batch: args.sources,
915
+ runDiscovery: args.runDiscovery,
576
916
  };
577
917
  }
578
- });
579
- // 10. Tool: explain_claim_relationship
580
- registerTool('explain_claim_relationship', 'Explains why two claims do or do not contradict each other based on context dimensions including environment, scope, source role, temporal state, SemVer range compatibility, and set membership.', {
581
- claimAId: z.string().uuid().describe('ID of the first claim'),
582
- claimBId: z.string().uuid().describe('ID of the second claim'),
583
- }, async (args) => {
584
- try {
585
- logger.debug('Executing explain_claim_relationship tool', args);
586
- if (!options.analysisService) {
587
- throw new Error('AnalysisService is not configured on this server instance');
588
- }
589
- const result = options.analysisService.explainClaimRelationship(args.claimAId, args.claimBId);
590
- return {
591
- content: [
592
- {
593
- type: 'text',
594
- text: JSON.stringify(result, null, 2),
595
- },
596
- ],
918
+ else if (toolName === 'scan_for_contradictions') {
919
+ request.params.name = 'scan_contradictions';
920
+ request.params.arguments = {
921
+ limit: args.limit,
922
+ minConfidence: args.minConfidence,
923
+ includeDismissed: args.includeDismissed,
597
924
  };
598
925
  }
599
- catch (error) {
600
- logger.error('Error during explain_claim_relationship execution', { error: String(error) });
601
- const safe = toSafeError(error);
602
- return {
603
- isError: true,
604
- content: [
605
- {
606
- type: 'text',
607
- text: JSON.stringify({
608
- error: safe.message,
609
- code: safe.code,
610
- claimAId: args.claimAId,
611
- claimBId: args.claimBId,
612
- }, null, 2),
613
- },
614
- ],
926
+ else if (toolName === 'scan_claim_for_contradictions') {
927
+ request.params.name = 'scan_contradictions';
928
+ request.params.arguments = {
929
+ claimId: args.claimId,
930
+ minConfidence: args.minConfidence,
615
931
  };
616
932
  }
617
- });
618
- // 11. Tool: review_contradiction
619
- registerTool('review_contradiction', 'Marks a contradiction record as REVIEWED, recording the reviewer identity and optional notes in the persistent audit trail.', {
620
- contradictionId: z
621
- .string()
622
- .min(1)
623
- .describe('The unique ID of the contradiction record to review'),
624
- reviewedBy: z.string().min(1).describe('Name or identifier of the reviewer / agent'),
625
- notes: z.string().optional().describe('Review findings or triage notes'),
626
- }, async (args) => {
627
- try {
628
- checkScope('review', options);
629
- metricsService.recordToolCall('review_contradiction');
630
- if (!reviewService) {
631
- throw new Error('ReviewService is not configured on this server instance');
632
- }
633
- const updated = reviewService.reviewContradiction(args.contradictionId, {
634
- reviewedBy: args.reviewedBy,
635
- notes: args.notes,
636
- });
637
- return {
638
- isError: false,
639
- content: [
640
- {
641
- type: 'text',
642
- text: JSON.stringify(updated, null, 2),
643
- },
644
- ],
933
+ else if (toolName === 'scan_source_for_contradictions') {
934
+ request.params.name = 'scan_contradictions';
935
+ request.params.arguments = {
936
+ sourceId: args.sourceId,
937
+ minConfidence: args.minConfidence,
938
+ limit: args.limit,
939
+ includeDismissed: args.includeDismissed,
645
940
  };
646
941
  }
647
- catch (error) {
648
- logger.error('Error during review_contradiction execution', { error: String(error) });
649
- const safe = toSafeError(error);
650
- return {
651
- isError: true,
652
- content: [
653
- {
654
- type: 'text',
655
- text: JSON.stringify({
656
- error: safe.message,
657
- code: safe.code,
658
- contradictionId: args.contradictionId,
659
- }, null, 2),
660
- },
661
- ],
942
+ else if (toolName === 'explain_claim_relationship') {
943
+ request.params.name = 'analyze_claim_pair';
944
+ request.params.arguments = {
945
+ claimAId: args.claimAId,
946
+ claimBId: args.claimBId,
947
+ explainContext: true,
662
948
  };
663
949
  }
664
- });
665
- // 12. Tool: resolve_contradiction
666
- registerTool('resolve_contradiction', 'Resolves a contradiction record, recording the authoritative chosen claim (optional), resolution reason, and audit trail.', {
667
- contradictionId: z
668
- .string()
669
- .min(1)
670
- .describe('The unique ID of the contradiction record to resolve'),
671
- resolvedBy: z.string().min(1).describe('Name or identifier of the resolver'),
672
- reason: z
673
- .string()
674
- .min(1)
675
- .describe('Detailed explanation of why and how this contradiction was resolved'),
676
- chosenClaimId: z
677
- .string()
678
- .optional()
679
- .describe('ID of the claim accepted as authoritative (optional)'),
680
- notes: z.string().optional().describe('Additional resolution notes'),
681
- }, async (args) => {
682
- try {
683
- checkScope('resolve', options);
684
- metricsService.recordToolCall('resolve_contradiction');
685
- if (!reviewService) {
686
- throw new Error('ReviewService is not configured on this server instance');
687
- }
688
- const updated = reviewService.resolveContradiction(args.contradictionId, {
689
- resolvedBy: args.resolvedBy,
690
- reason: args.reason,
691
- chosenClaimId: args.chosenClaimId,
950
+ else if (toolName === 'review_contradiction') {
951
+ request.params.name = 'resolve_contradiction';
952
+ request.params.arguments = {
953
+ contradictionId: args.contradictionId,
954
+ action: 'REVIEW',
955
+ actor: args.reviewedBy,
692
956
  notes: args.notes,
693
- });
694
- return {
695
- isError: false,
696
- content: [
697
- {
698
- type: 'text',
699
- text: JSON.stringify(updated, null, 2),
700
- },
701
- ],
702
957
  };
703
958
  }
704
- catch (error) {
705
- logger.error('Error during resolve_contradiction execution', { error: String(error) });
706
- const safe = toSafeError(error);
707
- return {
708
- isError: true,
709
- content: [
710
- {
711
- type: 'text',
712
- text: JSON.stringify({
713
- error: safe.message,
714
- code: safe.code,
715
- contradictionId: args.contradictionId,
716
- }, null, 2),
717
- },
718
- ],
959
+ else if (toolName === 'dismiss_contradiction') {
960
+ request.params.name = 'resolve_contradiction';
961
+ request.params.arguments = {
962
+ contradictionId: args.contradictionId,
963
+ action: 'DISMISS',
964
+ actor: args.dismissedBy || 'reviewer',
965
+ reason: args.reason || 'Dismissed via legacy call',
966
+ notes: args.notes,
719
967
  };
720
968
  }
721
- });
722
- // 13. Tool: dismiss_contradiction
723
- registerTool('dismiss_contradiction', 'Dismisses a contradiction record as acceptable or non-actionable, preserving an audit record of the decision.', {
724
- contradictionId: z
725
- .string()
726
- .min(1)
727
- .describe('The unique ID of the contradiction record to dismiss'),
728
- dismissedBy: z
729
- .string()
730
- .min(1)
731
- .describe('Name or identifier of the actor dismissing this contradiction'),
732
- reason: z.string().min(1).describe('Reason why this contradiction is dismissed'),
733
- notes: z.string().optional().describe('Additional notes'),
734
- }, async (args) => {
735
- try {
736
- checkScope('review', options);
737
- metricsService.recordToolCall('dismiss_contradiction');
738
- if (!reviewService) {
739
- throw new Error('ReviewService is not configured on this server instance');
740
- }
741
- const updated = reviewService.dismissContradiction(args.contradictionId, {
742
- dismissedBy: args.dismissedBy,
969
+ else if (toolName === 'reopen_contradiction') {
970
+ request.params.name = 'resolve_contradiction';
971
+ request.params.arguments = {
972
+ contradictionId: args.contradictionId,
973
+ action: 'REOPEN',
974
+ actor: args.reopenedBy || 'reviewer',
743
975
  reason: args.reason,
744
976
  notes: args.notes,
745
- });
746
- return {
747
- isError: false,
748
- content: [
749
- {
750
- type: 'text',
751
- text: JSON.stringify(updated, null, 2),
752
- },
753
- ],
754
977
  };
755
978
  }
756
- catch (error) {
757
- logger.error('Error during dismiss_contradiction execution', { error: String(error) });
758
- const safe = toSafeError(error);
759
- return {
760
- isError: true,
761
- content: [
762
- {
763
- type: 'text',
764
- text: JSON.stringify({
765
- error: safe.message,
766
- code: safe.code,
767
- contradictionId: args.contradictionId,
768
- }, null, 2),
769
- },
770
- ],
979
+ else if (toolName === 'get_contradiction_history') {
980
+ request.params.name = 'get_contradiction';
981
+ request.params.arguments = {
982
+ contradictionId: args.contradictionId,
983
+ includeAuditHistory: true,
771
984
  };
772
985
  }
773
- });
774
- // 14. Tool: reopen_contradiction
775
- registerTool('reopen_contradiction', 'Reopens a previously resolved or dismissed contradiction back to OPEN status with audit trail logging.', {
776
- contradictionId: z
777
- .string()
778
- .min(1)
779
- .describe('The unique ID of the contradiction record to reopen'),
780
- reopenedBy: z
781
- .string()
782
- .min(1)
783
- .describe('Name or identifier of the actor reopening this contradiction'),
784
- reason: z.string().optional().describe('Reason for reopening'),
785
- notes: z.string().optional().describe('Additional notes'),
786
- }, async (args) => {
787
- try {
788
- checkScope('review', options);
789
- metricsService.recordToolCall('reopen_contradiction');
790
- if (!reviewService) {
791
- throw new Error('ReviewService is not configured on this server instance');
792
- }
793
- const updated = reviewService.reopenContradiction(args.contradictionId, {
794
- reopenedBy: args.reopenedBy,
795
- reason: args.reason,
796
- notes: args.notes,
797
- });
798
- return {
799
- isError: false,
800
- content: [
801
- {
802
- type: 'text',
803
- text: JSON.stringify(updated, null, 2),
804
- },
805
- ],
806
- };
807
- }
808
- catch (error) {
809
- logger.error('Error during reopen_contradiction execution', { error: String(error) });
810
- const safe = toSafeError(error);
811
- return {
812
- isError: true,
813
- content: [
814
- {
815
- type: 'text',
816
- text: JSON.stringify({
817
- error: safe.message,
818
- code: safe.code,
819
- contradictionId: args.contradictionId,
820
- }, null, 2),
821
- },
822
- ],
823
- };
824
- }
825
- });
826
- // 15. Tool: get_contradiction_history
827
- registerTool('get_contradiction_history', 'Retrieves the chronological audit history of review and resolution actions performed on a contradiction record.', {
828
- contradictionId: z.string().min(1).describe('The unique ID of the contradiction record'),
829
- }, async (args) => {
830
- try {
831
- metricsService.recordToolCall('get_contradiction_history');
832
- if (!reviewService) {
833
- throw new Error('ReviewService is not configured on this server instance');
834
- }
835
- const history = reviewService.getContradictionHistory(args.contradictionId);
836
- return {
837
- isError: false,
838
- content: [
839
- {
840
- type: 'text',
841
- text: JSON.stringify({
842
- contradictionId: args.contradictionId,
843
- count: history.length,
844
- history,
845
- }, null, 2),
846
- },
847
- ],
848
- };
849
- }
850
- catch (error) {
851
- logger.error('Error during get_contradiction_history execution', { error: String(error) });
852
- const safe = toSafeError(error);
853
- return {
854
- isError: true,
855
- content: [
856
- {
857
- type: 'text',
858
- text: JSON.stringify({
859
- error: safe.message,
860
- code: safe.code,
861
- contradictionId: args.contradictionId,
862
- }, null, 2),
863
- },
864
- ],
865
- };
866
- }
867
- });
868
- // 16. Tool: get_claim_history
869
- registerTool('get_claim_history', 'Retrieves the chronological value transition history for a specific factual claim, tracking updates and superseded values.', {
870
- claimId: z.string().min(1).describe('The unique ID of the claim'),
871
- }, async (args) => {
872
- try {
873
- metricsService.recordToolCall('get_claim_history');
874
- if (!options.dbManager) {
875
- throw new Error('DatabaseManager is not configured on this server instance');
876
- }
877
- const claim = options.dbManager.getClaimById(args.claimId);
878
- if (!claim) {
879
- throw new NotFoundError('Claim', args.claimId);
880
- }
881
- const history = options.dbManager.getClaimHistory(args.claimId);
882
- return {
883
- isError: false,
884
- content: [
885
- {
886
- type: 'text',
887
- text: JSON.stringify({
888
- claimId: args.claimId,
889
- currentValue: claim.value,
890
- normalizedValue: claim.normalizedValue,
891
- observedAt: claim.observedAt,
892
- firstSeenAt: claim.firstSeenAt,
893
- lastSeenAt: claim.lastSeenAt,
894
- supersededBy: claim.supersededBy,
895
- historyCount: history.length,
896
- history,
897
- }, null, 2),
898
- },
899
- ],
900
- };
901
- }
902
- catch (error) {
903
- logger.error('Error during get_claim_history execution', { error: String(error) });
904
- const safe = toSafeError(error);
905
- return {
906
- isError: true,
907
- content: [
908
- {
909
- type: 'text',
910
- text: JSON.stringify({
911
- error: safe.message,
912
- code: safe.code,
913
- claimId: args.claimId,
914
- }, null, 2),
915
- },
916
- ],
917
- };
918
- }
919
- });
920
- // 17. Tool: advise_resolution
921
- registerTool('advise_resolution', 'Generates deterministic authority and freshness scoring comparison to advise an agent on which claim likely represents current truth.', {
922
- contradictionId: z.string().min(1).describe('The unique ID of the contradiction record'),
923
- }, async (args) => {
924
- try {
925
- metricsService.recordToolCall('advise_resolution');
926
- if (!options.dbManager) {
927
- throw new Error('DatabaseManager is not configured on this server instance');
928
- }
929
- const details = options.dbManager.getContradictionWithDetails(args.contradictionId);
930
- if (!details) {
931
- throw new NotFoundError('Contradiction', args.contradictionId);
932
- }
933
- const advice = resolutionAdvisor.adviseResolution(details.contradiction, details.claimA, details.claimB, details.sourceA, details.sourceB);
934
- return {
935
- isError: false,
936
- content: [
937
- {
938
- type: 'text',
939
- text: JSON.stringify(advice, null, 2),
940
- },
941
- ],
942
- };
943
- }
944
- catch (error) {
945
- logger.error('Error during advise_resolution execution', { error: String(error) });
946
- const safe = toSafeError(error);
947
- return {
948
- isError: true,
949
- content: [
950
- {
951
- type: 'text',
952
- text: JSON.stringify({
953
- error: safe.message,
954
- code: safe.code,
955
- contradictionId: args.contradictionId,
956
- }, null, 2),
957
- },
958
- ],
959
- };
960
- }
961
- });
962
- // 18. Tool: sync_source
963
- registerTool('sync_source', 'Unified ingestion tool: synchronizes any registered source connector (github, document, website), idempotently extracts claims, and runs automatic contradiction discovery.', {
964
- connector: z
965
- .string()
966
- .min(1)
967
- .describe('The connector identifier (e.g. github, document, website)'),
968
- input: z.record(z.string(), z.unknown()).describe('Connector-specific input parameters'),
969
- runDiscovery: z
970
- .boolean()
971
- .optional()
972
- .describe('Whether to trigger automatic contradiction discovery (default: true)'),
973
- }, async (args) => {
974
- try {
975
- checkScope('sync', options);
976
- metricsService.recordToolCall('sync_source');
977
- if (!options.syncService) {
978
- throw new Error('SyncService is not configured on this server instance');
979
- }
980
- const summary = await options.syncService.syncSource(args.connector, args.input, {
981
- runDiscoveryAfterSync: args.runDiscovery ?? true,
982
- });
983
- metricsService.recordSync(summary.status === 'completed', summary.claimsCreated, summary.contradictionsFound ?? 0);
984
- return {
985
- isError: summary.status === 'failed',
986
- content: [
987
- {
988
- type: 'text',
989
- text: JSON.stringify(summary, null, 2),
990
- },
991
- ],
992
- };
993
- }
994
- catch (error) {
995
- logger.error('Error during sync_source execution', { error: String(error) });
996
- const safe = toSafeError(error);
997
- return {
998
- isError: true,
999
- content: [
1000
- {
1001
- type: 'text',
1002
- text: JSON.stringify({
1003
- error: safe.message,
1004
- code: safe.code,
1005
- connector: args.connector,
1006
- }, null, 2),
1007
- },
1008
- ],
1009
- };
1010
- }
1011
- });
1012
- // 19. Tool: sync_sources
1013
- registerTool('sync_sources', 'Batch ingestion tool: synchronizes multiple external sources with bounded concurrency, failure isolation, and unified contradiction discovery.', {
1014
- sources: z
1015
- .array(z.object({
1016
- connector: z.string().min(1).describe('Connector type: github, document, website'),
1017
- input: z.record(z.string(), z.unknown()).describe('Input parameters for connector'),
1018
- }))
1019
- .min(1)
1020
- .describe('List of source synchronization requests'),
1021
- concurrency: z
1022
- .number()
1023
- .int()
1024
- .positive()
1025
- .max(10)
1026
- .optional()
1027
- .describe('Max concurrent workers (default: 3)'),
1028
- runDiscovery: z
1029
- .boolean()
1030
- .optional()
1031
- .describe('Whether to run discovery after synchronization (default: true)'),
1032
- }, async (args) => {
1033
- try {
1034
- checkScope('sync', options);
1035
- metricsService.recordToolCall('sync_sources');
1036
- if (!options.syncService) {
1037
- throw new Error('SyncService is not configured on this server instance');
1038
- }
1039
- const summary = await options.syncService.syncSources(args.sources, {
1040
- concurrency: args.concurrency ?? 3,
1041
- runDiscoveryAfterSync: args.runDiscovery ?? true,
1042
- });
1043
- return {
1044
- isError: summary.sourcesFailed === summary.sourcesRequested && summary.sourcesRequested > 0,
1045
- content: [
1046
- {
1047
- type: 'text',
1048
- text: JSON.stringify(summary, null, 2),
1049
- },
1050
- ],
1051
- };
1052
- }
1053
- catch (error) {
1054
- logger.error('Error during sync_sources execution', { error: String(error) });
1055
- const safe = toSafeError(error);
1056
- return {
1057
- isError: true,
1058
- content: [
1059
- {
1060
- type: 'text',
1061
- text: JSON.stringify({
1062
- error: safe.message,
1063
- code: safe.code,
1064
- }, null, 2),
1065
- },
1066
- ],
1067
- };
1068
- }
1069
- });
1070
- // 20. Tool: sync_document
1071
- registerTool('sync_document', 'Synchronizes a local document file (Markdown, text, JSON, YAML, CSV), extracts claims with exact line provenance, and discovers contradictions.', {
1072
- filePath: z.string().min(1).describe('Absolute or relative file path on disk'),
1073
- sourceName: z.string().optional().describe('Optional friendly name for this source'),
1074
- subject: z
1075
- .string()
1076
- .optional()
1077
- .describe('Subject entity name for extracted claims (default: filename)'),
1078
- scope: z.string().optional().describe('Scope of the document (default: file)'),
1079
- environment: z
1080
- .string()
1081
- .optional()
1082
- .describe('Environment context (e.g. production, development)'),
1083
- sourceRole: z
1084
- .string()
1085
- .optional()
1086
- .describe('Source role (e.g. deployment, configuration, documentation)'),
1087
- runDiscovery: z
1088
- .boolean()
1089
- .optional()
1090
- .describe('Whether to trigger automatic contradiction discovery (default: true)'),
1091
- }, async (args) => {
1092
- try {
1093
- checkScope('sync', options);
1094
- metricsService.recordToolCall('sync_document');
1095
- if (!options.syncService) {
1096
- throw new Error('SyncService is not configured on this server instance');
1097
- }
1098
- const summary = await options.syncService.syncSource('document', {
1099
- filePath: args.filePath,
1100
- sourceName: args.sourceName,
1101
- subject: args.subject,
1102
- scope: args.scope,
1103
- environment: args.environment,
1104
- sourceRole: args.sourceRole,
1105
- }, {
1106
- runDiscoveryAfterSync: args.runDiscovery ?? true,
1107
- });
1108
- return {
1109
- isError: summary.status === 'failed',
1110
- content: [
1111
- {
1112
- type: 'text',
1113
- text: JSON.stringify(summary, null, 2),
1114
- },
1115
- ],
1116
- };
1117
- }
1118
- catch (error) {
1119
- logger.error('Error during sync_document execution', { error: String(error) });
1120
- const safe = toSafeError(error);
1121
- return {
1122
- isError: true,
1123
- content: [
1124
- {
1125
- type: 'text',
1126
- text: JSON.stringify({
1127
- error: safe.message,
1128
- code: safe.code,
1129
- filePath: args.filePath,
1130
- }, null, 2),
1131
- },
1132
- ],
1133
- };
1134
- }
1135
- });
1136
- // 21. Tool: sync_website
1137
- registerTool('sync_website', 'Fetches an explicit public web page with strict SSRF defenses, extracts factual claims, and discovers contradictions.', {
1138
- url: z.string().url().describe('Public web page URL (http or https only)'),
1139
- sourceName: z.string().optional().describe('Friendly name for the website'),
1140
- runDiscovery: z
1141
- .boolean()
1142
- .optional()
1143
- .describe('Whether to trigger automatic contradiction discovery (default: true)'),
1144
- }, async (args) => {
1145
- try {
1146
- checkScope('sync', options);
1147
- metricsService.recordToolCall('sync_website');
1148
- if (!options.syncService) {
1149
- throw new Error('SyncService is not configured on this server instance');
1150
- }
1151
- const summary = await options.syncService.syncSource('website', {
1152
- url: args.url,
1153
- sourceName: args.sourceName,
1154
- }, {
1155
- runDiscoveryAfterSync: args.runDiscovery ?? true,
1156
- });
1157
- return {
1158
- isError: summary.status === 'failed',
1159
- content: [
1160
- {
1161
- type: 'text',
1162
- text: JSON.stringify(summary, null, 2),
1163
- },
1164
- ],
1165
- };
1166
- }
1167
- catch (error) {
1168
- logger.error('Error during sync_website execution', { error: String(error) });
1169
- const safe = toSafeError(error);
1170
- return {
1171
- isError: true,
1172
- content: [
1173
- {
1174
- type: 'text',
1175
- text: JSON.stringify({
1176
- error: safe.message,
1177
- code: safe.code,
1178
- url: args.url,
1179
- }, null, 2),
1180
- },
1181
- ],
986
+ else if (toolName === 'get_claim_history') {
987
+ request.params.name = 'get_claim';
988
+ request.params.arguments = {
989
+ claimId: args.claimId,
990
+ includeHistory: true,
1182
991
  };
1183
992
  }
993
+ return canonicalCallHandler(request, ctx);
1184
994
  });
1185
995
  // --- MCP RESOURCES ---
1186
996
  server.registerResource('health-metrics', 'health://metrics', {
@@ -1290,7 +1100,7 @@ export function createMcpServer(options) {
1290
1100
  role: 'user',
1291
1101
  content: {
1292
1102
  type: 'text',
1293
- text: `Please investigate ${targetText} using Contradiction MCP tools:\n${step1}\n2. Inspect the conflicting claims and their originating sources.\n3. Call 'explain_claim_relationship' to analyze contextual dimensions (environment, scope, roles, ranges).\n4. Call 'advise_resolution' to assess authority and freshness scoring.\n5. Recommend or execute 'resolve_contradiction' or 'dismiss_contradiction' with justified reasoning.`,
1103
+ text: `Please investigate ${targetText} using Contradiction MCP tools:\n${step1}\n2. Inspect the conflicting claims and their originating sources.\n3. Call 'analyze_claim_pair' to analyze contextual dimensions (environment, scope, roles, ranges).\n4. Call 'advise_resolution' to assess authority and freshness scoring.\n5. Recommend or execute 'resolve_contradiction' with justified reasoning.`,
1294
1104
  },
1295
1105
  },
1296
1106
  ],
@@ -1316,7 +1126,7 @@ export function createMcpServer(options) {
1316
1126
  role: 'user',
1317
1127
  content: {
1318
1128
  type: 'text',
1319
- text: `Review consistency ${sourceTarget}:\n1. Run 'list_connectors' to review registered data sources.\n2. Run 'scan_for_contradictions' to discover any newly introduced discrepancies.\n3. Query 'list_contradictions' filtered by status='OPEN'.\n4. For each high or critical contradiction, review the conflicting evidence snippets and recommend updates.`,
1129
+ text: `Review consistency ${sourceTarget}:\n1. Run 'list_sources' to review registered data sources.\n2. Run 'scan_contradictions' to discover any newly introduced discrepancies.\n3. Query 'list_contradictions' filtered by status='OPEN'.\n4. For each high or critical contradiction, review the conflicting evidence snippets and recommend updates.`,
1320
1130
  },
1321
1131
  },
1322
1132
  ],