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/README.md +44 -41
- package/dist/config/env.js +1 -1
- package/dist/connectors/document/documentConnector.js +43 -8
- package/dist/connectors/document/documentConnector.js.map +1 -1
- package/dist/scripts/comprehensiveVerification.js +2 -2
- package/dist/server.js +624 -814
- package/dist/server.js.map +1 -1
- package/dist/storage/database.d.ts +4 -0
- package/dist/storage/database.js +20 -0
- package/dist/storage/database.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
//
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
80
|
-
registerTool('
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
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
|
|
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:
|
|
123
|
-
registerTool('
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.
|
|
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('
|
|
148
|
+
.describe('Optional git branch or tag name to check when testing GitHub repositories'),
|
|
140
149
|
}, async (args) => {
|
|
141
150
|
try {
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
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
|
-
|
|
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:
|
|
203
|
+
isError: true,
|
|
149
204
|
content: [
|
|
150
205
|
{
|
|
151
206
|
type: 'text',
|
|
152
|
-
text: JSON.stringify(
|
|
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
|
|
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
|
-
//
|
|
175
|
-
registerTool('
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
454
|
+
claimAId: args.claimAId,
|
|
455
|
+
claimBId: args.claimBId,
|
|
216
456
|
}, null, 2),
|
|
217
457
|
},
|
|
218
458
|
],
|
|
219
459
|
};
|
|
220
460
|
}
|
|
221
461
|
});
|
|
222
|
-
//
|
|
223
|
-
registerTool('
|
|
224
|
-
|
|
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('
|
|
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
|
|
240
|
-
|
|
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
|
|
533
|
+
.describe('Whether to include full chronological value transitions and superseded historical values (default: true)'),
|
|
244
534
|
}, async (args) => {
|
|
245
535
|
try {
|
|
246
|
-
|
|
247
|
-
if (!options.
|
|
248
|
-
throw new Error('
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
286
|
-
registerTool('list_contradictions', 'Queries stored contradiction records from the database with
|
|
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
|
-
|
|
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
|
-
//
|
|
393
|
-
registerTool('get_contradiction', 'Retrieves a single contradiction by ID, including
|
|
394
|
-
contradictionId: z
|
|
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
|
-
|
|
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
|
-
//
|
|
434
|
-
registerTool('
|
|
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
|
-
|
|
437
|
-
if (!options.
|
|
438
|
-
throw new Error('
|
|
439
|
-
}
|
|
440
|
-
const
|
|
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
|
|
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
|
-
//
|
|
472
|
-
registerTool('
|
|
473
|
-
|
|
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
|
|
477
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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 '
|
|
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 '
|
|
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
|
],
|