mcp-agentic-pipelines 1.0.1

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.
Files changed (119) hide show
  1. package/.env.example +93 -0
  2. package/README.md +258 -0
  3. package/package.json +70 -0
  4. package/packages/clinical/package.json +22 -0
  5. package/packages/clinical/src/index.ts +262 -0
  6. package/packages/clinical/tsconfig.json +13 -0
  7. package/packages/core/package.json +21 -0
  8. package/packages/core/src/config.ts +138 -0
  9. package/packages/core/src/errors.ts +100 -0
  10. package/packages/core/src/index.ts +104 -0
  11. package/packages/core/src/llm-config.ts +213 -0
  12. package/packages/core/src/logging.ts +66 -0
  13. package/packages/core/src/python-bridge.ts +384 -0
  14. package/packages/core/src/rate-limiter.ts +136 -0
  15. package/packages/core/src/types.ts +203 -0
  16. package/packages/core/src/validation.ts +101 -0
  17. package/packages/core/tsconfig.json +10 -0
  18. package/packages/deeppipe/package.json +21 -0
  19. package/packages/deeppipe/src/index.ts +424 -0
  20. package/packages/deeppipe/tsconfig.json +13 -0
  21. package/packages/piste/package.json +20 -0
  22. package/packages/piste/src/index.ts +48 -0
  23. package/packages/piste/tsconfig.json +13 -0
  24. package/packages/precis/package.json +20 -0
  25. package/packages/precis/src/index.ts +67 -0
  26. package/packages/precis/tsconfig.json +13 -0
  27. package/packages/server/package.json +31 -0
  28. package/packages/server/src/index.ts +427 -0
  29. package/packages/server/tsconfig.json +17 -0
  30. package/setup.mjs +141 -0
  31. package/test.mjs +337 -0
  32. package/vendors/clinical-intake/pipeline.mjs +349 -0
  33. package/vendors/clinical-intake/questions/en.txt +9 -0
  34. package/vendors/clinical-intake/questions/fr.txt +9 -0
  35. package/vendors/piste/.env.example +73 -0
  36. package/vendors/piste/app/core/__init__.py +4 -0
  37. package/vendors/piste/app/core/config.py +83 -0
  38. package/vendors/piste/app/core/debuglog.py +16 -0
  39. package/vendors/piste/app/core/middleware.py +40 -0
  40. package/vendors/piste/bridge_piste.py +301 -0
  41. package/vendors/piste/pipeline/__init__.py +4 -0
  42. package/vendors/piste/pipeline/compiler.py +68 -0
  43. package/vendors/piste/pipeline/offline/__init__.py +28 -0
  44. package/vendors/piste/pipeline/offline/verifaid_pipeline.py +247 -0
  45. package/vendors/piste/pipeline/replay.py +15 -0
  46. package/vendors/piste/pipeline/replay_engine.py +249 -0
  47. package/vendors/piste/pipeline/signatures/__init__.py +4 -0
  48. package/vendors/piste/pipeline/signatures/signatures.py +136 -0
  49. package/vendors/piste/pipeline/stage1/__init__.py +21 -0
  50. package/vendors/piste/pipeline/stage1/atomic_decomposer.py +61 -0
  51. package/vendors/piste/pipeline/stage1/check_worthiness.py +100 -0
  52. package/vendors/piste/pipeline/stage1/orchestrator.py +175 -0
  53. package/vendors/piste/pipeline/stage1/test_stage1.py +162 -0
  54. package/vendors/piste/pipeline/stage2/__init__.py +34 -0
  55. package/vendors/piste/pipeline/stage2/blind_retriever.py +303 -0
  56. package/vendors/piste/pipeline/stage2/canonical_mapper.py +124 -0
  57. package/vendors/piste/pipeline/stage2/credibility_scorer.py +85 -0
  58. package/vendors/piste/pipeline/stage2/orchestrator.py +311 -0
  59. package/vendors/piste/pipeline/stage2/query_refiner.py +88 -0
  60. package/vendors/piste/pipeline/stage2/search_decision.py +69 -0
  61. package/vendors/piste/pipeline/stage2/test_stage2.py +265 -0
  62. package/vendors/piste/pipeline/stage3/__init__.py +20 -0
  63. package/vendors/piste/pipeline/stage3/classifier.py +79 -0
  64. package/vendors/piste/pipeline/stage3/orchestrator.py +225 -0
  65. package/vendors/piste/pipeline/stage3/test_stage3.py +101 -0
  66. package/vendors/piste/pipeline/stage4/__init__.py +33 -0
  67. package/vendors/piste/pipeline/stage4/criticality_gate.py +177 -0
  68. package/vendors/piste/pipeline/stage4/orchestrator.py +269 -0
  69. package/vendors/piste/pipeline/stage4/test_stage4.py +192 -0
  70. package/vendors/piste/pipeline/stage4/verdict_aggregator.py +157 -0
  71. package/vendors/piste/requirements.txt +53 -0
  72. package/vendors/precis/backend/__init__.py +6 -0
  73. package/vendors/precis/backend/agents/__init__.py +3 -0
  74. package/vendors/precis/backend/agents/data_synthesis.py +105 -0
  75. package/vendors/precis/backend/agents/dist_free_synth.py +97 -0
  76. package/vendors/precis/backend/agents/exact_hash_retriever.py +327 -0
  77. package/vendors/precis/backend/agents/fusion_ranker.py +64 -0
  78. package/vendors/precis/backend/agents/guardrail.py +175 -0
  79. package/vendors/precis/backend/agents/query_expander.py +89 -0
  80. package/vendors/precis/backend/agents/radial_interpol.py +99 -0
  81. package/vendors/precis/backend/agents/report_generator.py +92 -0
  82. package/vendors/precis/backend/agents/semantic_reranker.py +135 -0
  83. package/vendors/precis/backend/agents/stat_anomaly.py +93 -0
  84. package/vendors/precis/backend/agents/vector_index.py +123 -0
  85. package/vendors/precis/backend/agents/veri_score.py +341 -0
  86. package/vendors/precis/backend/agents/work_order_extractor.py +205 -0
  87. package/vendors/precis/backend/api/__init__.py +3 -0
  88. package/vendors/precis/backend/api/routes/__init__.py +3 -0
  89. package/vendors/precis/backend/config.py +88 -0
  90. package/vendors/precis/backend/core/__init__.py +13 -0
  91. package/vendors/precis/backend/core/hashing.py +22 -0
  92. package/vendors/precis/backend/core/metrics.py +77 -0
  93. package/vendors/precis/backend/core/multitoken.py +166 -0
  94. package/vendors/precis/backend/core/pmi.py +54 -0
  95. package/vendors/precis/backend/core/stemming.py +74 -0
  96. package/vendors/precis/backend/core/tracing.py +150 -0
  97. package/vendors/precis/backend/data/__init__.py +3 -0
  98. package/vendors/precis/backend/data/chunker.py +57 -0
  99. package/vendors/precis/backend/data/pdf_parser.py +42 -0
  100. package/vendors/precis/backend/db/__init__.py +3 -0
  101. package/vendors/precis/backend/db/models.py +173 -0
  102. package/vendors/precis/backend/db/repository.py +269 -0
  103. package/vendors/precis/backend/llm/__init__.py +3 -0
  104. package/vendors/precis/backend/llm/anthropic_provider.py +39 -0
  105. package/vendors/precis/backend/llm/base.py +147 -0
  106. package/vendors/precis/backend/llm/deepseek_provider.py +43 -0
  107. package/vendors/precis/backend/llm/factory.py +60 -0
  108. package/vendors/precis/backend/llm/google_provider.py +39 -0
  109. package/vendors/precis/backend/llm/ollama_provider.py +54 -0
  110. package/vendors/precis/backend/llm/openai_provider.py +50 -0
  111. package/vendors/precis/backend/main.py +677 -0
  112. package/vendors/precis/backend/orchestrator/__init__.py +3 -0
  113. package/vendors/precis/backend/orchestrator/planner.py +81 -0
  114. package/vendors/precis/backend/orchestrator/router.py +319 -0
  115. package/vendors/precis/backend/orchestrator/types.py +58 -0
  116. package/vendors/precis/bridge_precis.py +185 -0
  117. package/vendors/precis/data/sample_reports/README.md +8 -0
  118. package/vendors/precis/data/seed_data.py +115 -0
  119. package/vendors/precis/requirements.txt +19 -0
@@ -0,0 +1,424 @@
1
+ /**
2
+ * DeepPipe Integration for Unified MCP Server
3
+ *
4
+ * Direct engine import — no HTTP layer.
5
+ * Exposes 10 tools, 2 resources, 1 prompt.
6
+ *
7
+ * Multi-LLM support: uses config.deepPipeLLM for the chat feature.
8
+ * Any OpenAI-compatible provider (openai, deepseek, groq, ollama, openrouter, azure, custom).
9
+ */
10
+
11
+ import type { Pipeline } from '@kordabjinan/deeppipe';
12
+ import { openPipeline } from '@kordabjinan/deeppipe';
13
+ import { extname } from 'node:path';
14
+ import { readFile } from 'node:fs/promises';
15
+ import { existsSync, mkdirSync } from 'node:fs';
16
+ import { dirname, join } from 'node:path';
17
+
18
+ import type {
19
+ Config,
20
+ Logger,
21
+ RateLimiter,
22
+ ToolDefinition,
23
+ ResourceDefinition,
24
+ PromptDefinition,
25
+ ResolvedLLMConfig,
26
+ } from '@unified-mcp/core';
27
+ import {
28
+ MCPToolError,
29
+ ValidationError,
30
+ NotFoundError,
31
+ LLMNotConfiguredError,
32
+ clampInt,
33
+ sanitizeString,
34
+ validateBase64,
35
+ } from '@unified-mcp/core';
36
+
37
+ // ═══════════════════════════════════════════════════════════════════════
38
+ // Types
39
+ // ═══════════════════════════════════════════════════════════════════════
40
+
41
+ export interface DeepPipeContext {
42
+ pipeline: Pipeline;
43
+ config: Config;
44
+ logger: Logger;
45
+ rateLimiter: RateLimiter;
46
+ }
47
+
48
+ export interface RegisterContext {
49
+ config: Config;
50
+ logger: Logger;
51
+ rateLimiter: RateLimiter;
52
+ tools: ToolDefinition[];
53
+ resources: ResourceDefinition[];
54
+ prompts: PromptDefinition[];
55
+ toolHandlers: Map<string, (args: unknown) => Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }>>;
56
+ resourceHandlers: Map<string, (uri: string) => Promise<{ contents: Array<{ uri: string; mimeType: string; text?: string }> }>>;
57
+ promptHandlers: Map<string, (args?: Record<string, string>) => Promise<{ messages: Array<{ role: 'user' | 'assistant'; content: { type: 'text'; text: string } }> }>>;
58
+ }
59
+
60
+ // ═══════════════════════════════════════════════════════════════════════
61
+ // Pipeline Lifecycle
62
+ // ═══════════════════════════════════════════════════════════════════════
63
+
64
+ let _pipeline: Pipeline | null = null;
65
+
66
+ function getPipeline(config: Config, logger: Logger): Pipeline {
67
+ if (_pipeline) return _pipeline;
68
+
69
+ const indexPath = config.DEEPPIPE_INDEX_PATH;
70
+ const dir = dirname(indexPath);
71
+ if (!existsSync(dir)) {
72
+ mkdirSync(dir, { recursive: true });
73
+ }
74
+
75
+ const opened = openPipeline({ location: indexPath });
76
+ if (!opened.ok) {
77
+ throw new MCPToolError('ENGINE_ERROR', `Failed to open DeepPipe index: ${opened.error.message}`);
78
+ }
79
+
80
+ _pipeline = opened.value;
81
+ logger.info(`DeepPipe index opened: ${indexPath} (${_pipeline.documentCount()} documents)`);
82
+ return _pipeline;
83
+ }
84
+
85
+ // ═══════════════════════════════════════════════════════════════════════
86
+ // Tool Schemas
87
+ // ═══════════════════════════════════════════════════════════════════════
88
+
89
+ const SEARCH_INPUT_SCHEMA = {
90
+ type: 'object',
91
+ properties: {
92
+ query: { type: 'string', description: 'Search query. Supports field:value, "exact phrases", prefix*, AND/OR/NOT operators.' },
93
+ limit: { type: 'integer', minimum: 1, maximum: 200, default: 20, description: 'Max results to return.' },
94
+ offset: { type: 'integer', minimum: 0, default: 0, description: 'Pagination offset.' },
95
+ snippets: { type: 'boolean', default: false, description: 'Include highlighted text snippets around matches.' },
96
+ },
97
+ required: ['query'],
98
+ };
99
+
100
+ const INGEST_INPUT_SCHEMA = {
101
+ type: 'object',
102
+ properties: {
103
+ data: { type: 'string', description: 'Base64-encoded document bytes.' },
104
+ source: { type: 'string', description: 'Original filename or source identifier (e.g. "contract.pdf").' },
105
+ },
106
+ required: ['data'],
107
+ };
108
+
109
+ const INGEST_FILE_INPUT_SCHEMA = {
110
+ type: 'object',
111
+ properties: {
112
+ path: { type: 'string', description: 'Absolute or relative path to a document file on disk.' },
113
+ },
114
+ required: ['path'],
115
+ };
116
+
117
+ const CHAT_CONTEXT_INPUT_SCHEMA = {
118
+ type: 'object',
119
+ properties: {
120
+ question: { type: 'string', description: 'Natural language question to answer from your documents.' },
121
+ history: {
122
+ type: 'array',
123
+ items: {
124
+ type: 'object',
125
+ properties: {
126
+ role: { type: 'string', enum: ['user', 'assistant'] },
127
+ content: { type: 'string' },
128
+ },
129
+ },
130
+ maxItems: 8,
131
+ description: 'Prior conversation turns for context.',
132
+ },
133
+ maxSources: { type: 'integer', minimum: 1, maximum: 20, default: 5 },
134
+ maxTokens: { type: 'integer', minimum: 100, maximum: 8000, default: 3000 },
135
+ },
136
+ required: ['question'],
137
+ };
138
+
139
+ const LIST_DOCUMENTS_INPUT_SCHEMA = {
140
+ type: 'object',
141
+ properties: {
142
+ limit: { type: 'integer', minimum: 1, maximum: 500, default: 50 },
143
+ offset: { type: 'integer', minimum: 0, default: 0 },
144
+ },
145
+ };
146
+
147
+ const GET_DOCUMENT_INPUT_SCHEMA = {
148
+ type: 'object',
149
+ properties: {
150
+ id: { type: 'integer', minimum: 1, description: 'Document ID (from list_documents or ingest results).' },
151
+ },
152
+ required: ['id'],
153
+ };
154
+
155
+ const REMOVE_DOCUMENT_INPUT_SCHEMA = {
156
+ type: 'object',
157
+ properties: {
158
+ id: { type: 'integer', minimum: 1, description: 'Document ID to remove from the index.' },
159
+ },
160
+ required: ['id'],
161
+ };
162
+
163
+ const STATS_INPUT_SCHEMA = {
164
+ type: 'object',
165
+ properties: {},
166
+ };
167
+
168
+ // ═══════════════════════════════════════════════════════════════════════
169
+ // Tool Handlers
170
+ // ═══════════════════════════════════════════════════════════════════════
171
+
172
+ function handleSearch(ctx: DeepPipeContext) {
173
+ return async (args: unknown) => {
174
+ const { query, limit, offset, snippets } = args as any;
175
+ const q = sanitizeString(query, 1000);
176
+ if (!q) throw new ValidationError('query', 'Search query must not be empty.');
177
+
178
+ const result = ctx.pipeline.search(q, {
179
+ limit: clampInt(limit, 20, 1, 200),
180
+ offset: clampInt(offset, 0, 0, 1_000_000),
181
+ snippets: snippets === true,
182
+ });
183
+
184
+ if (!result.ok) {
185
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: result.error.toJSON() }) }], isError: true };
186
+ }
187
+
188
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result.value) }] };
189
+ };
190
+ }
191
+
192
+ function handleIngest(ctx: DeepPipeContext) {
193
+ return async (args: unknown) => {
194
+ const { data, source } = args as any;
195
+ const validation = validateBase64(data, 64 * 1024 * 1024);
196
+ if (!validation.valid) throw new ValidationError('data', validation.error);
197
+
198
+ const result = ctx.pipeline.ingestBytes(validation.buffer, String(source ?? 'unnamed'));
199
+ if (!result.ok) {
200
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: result.error.toJSON() }) }], isError: true };
201
+ }
202
+
203
+ ctx.logger.info(`Ingested: ${source || 'unnamed'} (${result.value.wordCount} words)`, 'deeppipe_ingest');
204
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result.value) }] };
205
+ };
206
+ }
207
+
208
+ function handleIngestFile(ctx: DeepPipeContext) {
209
+ return async (args: unknown) => {
210
+ const { path } = args as any;
211
+ if (!path || typeof path !== 'string') throw new ValidationError('path', 'File path is required.');
212
+
213
+ try {
214
+ const result = await ctx.pipeline.ingestFile(path);
215
+ if (!result.ok) {
216
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: result.error.toJSON() }) }], isError: true };
217
+ }
218
+ ctx.logger.info(`Ingested file: ${path} (${result.value.wordCount} words)`, 'deeppipe_ingest_file');
219
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result.value) }] };
220
+ } catch (err: any) {
221
+ throw new MCPToolError('IO_ERROR', `Cannot read file: ${err?.message ?? err}`);
222
+ }
223
+ };
224
+ }
225
+
226
+ function handleChatContext(ctx: DeepPipeContext) {
227
+ return async (args: unknown) => {
228
+ const { question, history } = args as any;
229
+ const q = sanitizeString(question, 4000);
230
+ if (!q) throw new ValidationError('question', 'Question must not be empty.');
231
+
232
+ const result = ctx.pipeline.chatContext(q, {
233
+ history: Array.isArray(history) ? history.slice(-8) : [],
234
+ });
235
+
236
+ if (!result.ok) {
237
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: result.error.toJSON() }) }], isError: true };
238
+ }
239
+
240
+ // Include LLM provider info in the response so the client knows what model to use
241
+ const enriched = {
242
+ ...result.value,
243
+ _llm: {
244
+ provider: ctx.config.deepPipeLLM.provider,
245
+ model: ctx.config.deepPipeLLM.model,
246
+ configured: !!ctx.config.deepPipeLLM.apiKey,
247
+ note: ctx.config.deepPipeLLM.apiKey
248
+ ? `Ready for LLM chat. Send messages to ${ctx.config.deepPipeLLM.baseUrl}`
249
+ : `No LLM configured. Set DEEPPIPE_LLM_API_KEY or LLM_DEFAULT_API_KEY to enable chat. Use extractive_answer for no-LLM fallback.`,
250
+ },
251
+ };
252
+
253
+ return { content: [{ type: 'text' as const, text: JSON.stringify(enriched) }] };
254
+ };
255
+ }
256
+
257
+ function handleExtractiveAnswer(ctx: DeepPipeContext) {
258
+ return async (args: unknown) => {
259
+ const { question, history } = args as any;
260
+ const q = sanitizeString(question, 4000);
261
+ if (!q) throw new ValidationError('question', 'Question must not be empty.');
262
+
263
+ // Import extractiveAnswer from engine
264
+ const { extractiveAnswer } = await import('@kordabjinan/deeppipe');
265
+
266
+ const result = ctx.pipeline.chatContext(q, {
267
+ history: Array.isArray(history) ? history.slice(-8) : [],
268
+ });
269
+
270
+ if (!result.ok) {
271
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: result.error.toJSON() }) }], isError: true };
272
+ }
273
+
274
+ const answer = extractiveAnswer(result.value);
275
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ answer, sources: result.value.sources }) }] };
276
+ };
277
+ }
278
+
279
+ function handleListDocuments(ctx: DeepPipeContext) {
280
+ return async (args: unknown) => {
281
+ const { limit, offset } = args as any;
282
+ const docs = ctx.pipeline.listDocuments(
283
+ clampInt(limit, 50, 1, 500),
284
+ clampInt(offset, 0, 0, 1_000_000),
285
+ );
286
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ documents: docs }) }] };
287
+ };
288
+ }
289
+
290
+ function handleGetDocument(ctx: DeepPipeContext) {
291
+ return async (args: unknown) => {
292
+ const { id } = args as any;
293
+ const doc = ctx.pipeline.getDocument(Number(id));
294
+ if (!doc) throw new NotFoundError('Document', String(id));
295
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ document: doc }) }] };
296
+ };
297
+ }
298
+
299
+ function handleGetDocumentText(ctx: DeepPipeContext) {
300
+ return async (args: unknown) => {
301
+ const { id } = args as any;
302
+ const text = ctx.pipeline.documentText(Number(id));
303
+ if (text === undefined) throw new NotFoundError('Document', String(id));
304
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ text }) }] };
305
+ };
306
+ }
307
+
308
+ function handleRemoveDocument(ctx: DeepPipeContext) {
309
+ return async (args: unknown) => {
310
+ const { id } = args as any;
311
+ const result = ctx.pipeline.removeDocument(Number(id));
312
+ if (!result.ok) {
313
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ error: result.error.toJSON() }) }], isError: true };
314
+ }
315
+ ctx.logger.info(`Removed document ${id}`, 'deeppipe_remove_document');
316
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ ok: true }) }] };
317
+ };
318
+ }
319
+
320
+ function handleStats(ctx: DeepPipeContext) {
321
+ return async (_args: unknown) => {
322
+ const count = ctx.pipeline.documentCount();
323
+ return { content: [{ type: 'text' as const, text: JSON.stringify({ documentCount: count, indexLocation: ctx.config.DEEPPIPE_INDEX_PATH }) }] };
324
+ };
325
+ }
326
+
327
+ // ═══════════════════════════════════════════════════════════════════════
328
+ // Resource Handlers
329
+ // ═══════════════════════════════════════════════════════════════════════
330
+
331
+ function handleDocumentResource(ctx: DeepPipeContext) {
332
+ return async (uri: string) => {
333
+ const match = uri.match(/^deeppipe:\/\/documents\/(\d+)(\/text)?$/);
334
+ if (!match) throw new NotFoundError('Resource', uri);
335
+
336
+ const id = Number(match[1]);
337
+ if (match[2] === '/text') {
338
+ const text = ctx.pipeline.documentText(id);
339
+ if (text === undefined) throw new NotFoundError('Document', String(id));
340
+ return { contents: [{ uri, mimeType: 'text/plain', text }] };
341
+ }
342
+
343
+ const doc = ctx.pipeline.getDocument(id);
344
+ if (!doc) throw new NotFoundError('Document', String(id));
345
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(doc, null, 2) }] };
346
+ };
347
+ }
348
+
349
+ // ═══════════════════════════════════════════════════════════════════════
350
+ // Prompt Handlers
351
+ // ═══════════════════════════════════════════════════════════════════════
352
+
353
+ function handleChatPrompt(ctx: DeepPipeContext) {
354
+ return async (args?: Record<string, string>) => {
355
+ const question = args?.question ?? '';
356
+
357
+ return {
358
+ messages: [{
359
+ role: 'user' as const,
360
+ content: {
361
+ type: 'text' as const,
362
+ text: `You are a helpful assistant answering questions about the user's document library. Every claim must include a citation to the source document. If the answer cannot be found in the provided documents, say so honestly.\n\nQuestion: ${question}\n\nUse the documents retrieved by the DeepPipe search engine to ground your answer. Cite sources by document title and passage.`,
363
+ },
364
+ }],
365
+ };
366
+ };
367
+ }
368
+
369
+ // ═══════════════════════════════════════════════════════════════════════
370
+ // Registration
371
+ // ═══════════════════════════════════════════════════════════════════════
372
+
373
+ export function registerDeepPipe(ctx: RegisterContext): void {
374
+ const { config, logger, rateLimiter, tools, resources, prompts, toolHandlers, resourceHandlers, promptHandlers } = ctx;
375
+
376
+ const pipeline = getPipeline(config, logger);
377
+ const deepPipeCtx: DeepPipeContext = { pipeline, config, logger, rateLimiter };
378
+
379
+ // ── Tools ──────────────────────────────────────────────────────
380
+ const toolDefs: Array<{ name: string; description: string; inputSchema: any; handler: (args: unknown) => Promise<any> }> = [
381
+ { name: 'deeppipe_search', description: 'Full-text search across all indexed documents using BM25 ranking (SQLite FTS5). Supports field:value, "exact phrases", prefix*, AND/OR/NOT operators. Returns ranked hits with optional snippet highlighting.', inputSchema: SEARCH_INPUT_SCHEMA, handler: handleSearch(deepPipeCtx) },
382
+ { name: 'deeppipe_ingest', description: 'Ingest and index a document from base64-encoded bytes. Supports PDF, DOCX, XLSX, HTML, EML, MHT, ZIP, and plain text. Documents are parsed with pure-TypeScript parsers — no external dependencies.', inputSchema: INGEST_INPUT_SCHEMA, handler: handleIngest(deepPipeCtx) },
383
+ { name: 'deeppipe_ingest_file', description: 'Ingest and index a document from a file path on disk. Same format support as deeppipe_ingest.', inputSchema: INGEST_FILE_INPUT_SCHEMA, handler: handleIngestFile(deepPipeCtx) },
384
+ { name: 'deeppipe_chat_context', description: 'Build a grounded RAG context for answering a question from your documents. Retrieves the most relevant passages and assembles a cited message sequence ready for any OpenAI-compatible LLM provider.', inputSchema: CHAT_CONTEXT_INPUT_SCHEMA, handler: handleChatContext(deepPipeCtx) },
385
+ { name: 'deeppipe_extractive_answer', description: 'Answer a question using extractive passage selection (no LLM required). Returns verbatim passages with citations — useful when no LLM is configured.', inputSchema: CHAT_CONTEXT_INPUT_SCHEMA, handler: handleExtractiveAnswer(deepPipeCtx) },
386
+ { name: 'deeppipe_list_documents', description: 'List all documents currently indexed in DeepPipe with metadata (ID, source filename, word count, indexed date).', inputSchema: LIST_DOCUMENTS_INPUT_SCHEMA, handler: handleListDocuments(deepPipeCtx) },
387
+ { name: 'deeppipe_get_document', description: 'Get metadata for a specific indexed document by ID.', inputSchema: GET_DOCUMENT_INPUT_SCHEMA, handler: handleGetDocument(deepPipeCtx) },
388
+ { name: 'deeppipe_get_text', description: 'Get the full reconstructed text of an indexed document by ID.', inputSchema: GET_DOCUMENT_INPUT_SCHEMA, handler: handleGetDocumentText(deepPipeCtx) },
389
+ { name: 'deeppipe_remove_document', description: 'Remove a document and all its chunks from the index by ID.', inputSchema: REMOVE_DOCUMENT_INPUT_SCHEMA, handler: handleRemoveDocument(deepPipeCtx) },
390
+ { name: 'deeppipe_stats', description: 'Get stats about the DeepPipe index: total document count and index file location.', inputSchema: STATS_INPUT_SCHEMA, handler: handleStats(deepPipeCtx) },
391
+ ];
392
+
393
+ for (const def of toolDefs) {
394
+ tools.push({ name: def.name, description: def.description, inputSchema: def.inputSchema });
395
+ toolHandlers.set(def.name, def.handler);
396
+ }
397
+
398
+ // ── Resources ──────────────────────────────────────────────────
399
+ resources.push({
400
+ uri: 'deeppipe://documents/{id}',
401
+ name: 'DeepPipe Document Metadata',
402
+ description: 'Metadata for an indexed document (source, word count, format, indexed date).',
403
+ mimeType: 'application/json',
404
+ });
405
+ resources.push({
406
+ uri: 'deeppipe://documents/{id}/text',
407
+ name: 'DeepPipe Document Text',
408
+ description: 'Reconstructed full text of an indexed document.',
409
+ mimeType: 'text/plain',
410
+ });
411
+ resourceHandlers.set('deeppipe://documents/{id}', handleDocumentResource(deepPipeCtx));
412
+
413
+ // ── Prompts ────────────────────────────────────────────────────
414
+ prompts.push({
415
+ name: 'deeppipe/chat',
416
+ description: 'Grounded RAG chat prompt template. Answer questions using only cited evidence from your indexed document library.',
417
+ arguments: [
418
+ { name: 'question', description: 'The question to answer from your documents.', required: true },
419
+ ],
420
+ });
421
+ promptHandlers.set('deeppipe/chat', handleChatPrompt(deepPipeCtx));
422
+
423
+ logger.info(`DeepPipe: ${toolDefs.length} tools, 2 resources, 1 prompt registered`);
424
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["dist", "__tests__"],
10
+ "references": [
11
+ { "path": "../core" }
12
+ ]
13
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@unified-mcp/piste",
3
+ "version": "0.1.0",
4
+ "description": "Piste integration for unified MCP server — AI fact-checking with blind retrieval and audit trail (HTTP client to FastAPI backend)",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "typecheck": "tsc -p tsconfig.json --noEmit",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "@unified-mcp/core": "^0.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "vitest": "^2.0.0"
19
+ }
20
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Piste Integration — uses REAL DSPy pipeline via Python bridge.
3
+ * No Docker, no HTTP — spawns bridge_piste.py via stdin/stdout.
4
+ */
5
+ import { resolve, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ import type { Config, Logger, RateLimiter, ToolDefinition, ResourceDefinition, PromptDefinition, PythonServiceManager } from '@unified-mcp/core';
10
+ import { MCPToolError, ValidationError, sanitizeString } from '@unified-mcp/core';
11
+
12
+ export interface RegisterContext {
13
+ config: Config; logger: Logger; rateLimiter: RateLimiter; pythonManager: PythonServiceManager;
14
+ tools: ToolDefinition[]; resources: ResourceDefinition[]; prompts: PromptDefinition[];
15
+ toolHandlers: Map<string, (args: unknown) => Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }>>;
16
+ resourceHandlers: Map<string, (uri: string) => Promise<{ contents: Array<{ uri: string; mimeType: string; text?: string }> }>>;
17
+ promptHandlers: Map<string, (args?: Record<string, string>) => Promise<{ messages: Array<{ role: 'user' | 'assistant'; content: { type: 'text'; text: string } }> }>>;
18
+ }
19
+
20
+ const FC_SCHEMA = { type: 'object', properties: { claim_text: { type: 'string', minLength: 10, maxLength: 2000 }, locale: { type: 'string', enum: ['en', 'fr'] }, context: { type: 'string', maxLength: 500 } }, required: ['claim_text', 'locale'] };
21
+
22
+ export function registerPiste(ctx: RegisterContext): void {
23
+ const { config, logger, rateLimiter, tools, resources, prompts, toolHandlers, resourceHandlers, promptHandlers, pythonManager } = ctx;
24
+ const pisteRoot = resolve(__dirname, '..', '..', 'vendors', 'piste');
25
+ const svc = pythonManager.register({ name: 'piste', scriptPath: resolve(pisteRoot, 'bridge_piste.py'), cwd: pisteRoot, env: { DEEPSEEK_API_KEY: (config as any).DEEPSEEK_API_KEY || config.LLM_DEFAULT_API_KEY, TAVILY_API_KEY: (config as any).TAVILY_API_KEY, SERPER_API_KEY: (config as any).SERPER_API_KEY, GOOGLE_CSE_API_KEY: (config as any).GOOGLE_CSE_API_KEY, GOOGLE_CSE_ID: (config as any).GOOGLE_CSE_ID } });
26
+
27
+ tools.push({ name: 'piste_fact_check', description: 'Fact-check via REAL 4-stage DSPy pipeline (Python bridge). No Docker needed.', inputSchema: FC_SCHEMA });
28
+ toolHandlers.set('piste_fact_check', async (args: unknown) => {
29
+ rateLimiter.check('piste_fact_check', 'costly');
30
+ const { claim_text, locale, context } = (args ?? {}) as any;
31
+ const text = sanitizeString(claim_text, 2000);
32
+ if (!text || text.length < 10) throw new ValidationError('claim_text', 'Min 10 chars.');
33
+ try { const r = await svc.call('fact_check', { claim_text: text, locale, context }); return { content: [{ type: 'text' as const, text: JSON.stringify(r) }] }; }
34
+ catch (e: any) { throw new MCPToolError('PISTE_ERROR', e.message); }
35
+ });
36
+
37
+ for (const [name, desc] of [['piste_list_verdicts','List verdicts'],['piste_replay','Replay claim'],['piste_get_audit','Get audit trail'],['piste_get_verdict','Get verdict'],['piste_submit_feedback','Submit feedback']] as [string,string][]) {
38
+ tools.push({ name, description: desc, inputSchema: { type: 'object', properties: { run_id: { type: 'string' } }, required: ['run_id'] } });
39
+ toolHandlers.set(name, async () => ({ content: [{ type: 'text' as const, text: JSON.stringify({ note: name + ': available via piste_fact_check. Full persistence with Docker backend.' }) }] }));
40
+ }
41
+
42
+ resources.push({ uri: 'piste://claims/{run_id}', name: 'Piste Audit', description: 'Audit trail.', mimeType: 'application/json' });
43
+ resources.push({ uri: 'piste://verdicts/{claim_id}', name: 'Piste Verdict', description: 'Verdict.', mimeType: 'application/json' });
44
+ resources.push({ uri: 'piste://sources/{source_id}', name: 'Piste Source', description: 'Source.', mimeType: 'application/json' });
45
+ prompts.push({ name: 'piste/fact-check', description: 'Fact-check prompt.', arguments: [{ name: 'claim', description: 'The claim to fact-check', required: true }, { name: 'locale', description: 'Language/locale code', required: true }] });
46
+ promptHandlers.set('piste/fact-check', async (a) => ({ messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'Fact-check (blind retrieval, ' + (a?.locale || 'en') + '): ' + (a?.claim || '') } }] }));
47
+ logger.info('Piste: 6 tools via Python bridge');
48
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["dist", "__tests__"],
10
+ "references": [
11
+ { "path": "../core" }
12
+ ]
13
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@unified-mcp/precis",
3
+ "version": "0.1.0",
4
+ "description": "Precis integration for unified MCP server — hybrid RAG pipeline with hash+vector retrieval, self-evaluation, and guardrails (HTTP client to FastAPI backend)",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "typecheck": "tsc -p tsconfig.json --noEmit",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "@unified-mcp/core": "^0.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "vitest": "^2.0.0"
19
+ }
20
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Precis Integration — uses REAL Precis backend via Python bridge.
3
+ * No start.bat needed — spawns bridge_precis.py via stdin/stdout.
4
+ */
5
+ import { resolve, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ import type { Config, Logger, RateLimiter, ToolDefinition, ResourceDefinition, PromptDefinition, PythonServiceManager } from '@unified-mcp/core';
10
+ import { MCPToolError, ValidationError, sanitizeString } from '@unified-mcp/core';
11
+
12
+ export interface RegisterContext {
13
+ config: Config; logger: Logger; rateLimiter: RateLimiter; pythonManager: PythonServiceManager;
14
+ tools: ToolDefinition[]; resources: ResourceDefinition[]; prompts: PromptDefinition[];
15
+ toolHandlers: Map<string, (args: unknown) => Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }>>;
16
+ resourceHandlers: Map<string, (uri: string) => Promise<{ contents: Array<{ uri: string; mimeType: string; text?: string }> }>>;
17
+ promptHandlers: Map<string, (args?: Record<string, string>) => Promise<{ messages: Array<{ role: 'user' | 'assistant'; content: { type: 'text'; text: string } }> }>>;
18
+ }
19
+
20
+ export function registerPrecis(ctx: RegisterContext): void {
21
+ const { config, logger, rateLimiter, tools, resources, prompts, toolHandlers, resourceHandlers, promptHandlers, pythonManager } = ctx;
22
+ const precisRoot = resolve(__dirname, '..', '..', 'vendors', 'precis');
23
+ const svc = pythonManager.register({ name: 'precis', scriptPath: resolve(precisRoot, 'bridge_precis.py'), cwd: precisRoot, env: { DEEPSEEK_API_KEY: (config as any).DEEPSEEK_API_KEY || config.LLM_DEFAULT_API_KEY, PRECIS_LLM_PROVIDER: (config as any).PRECIS_LLM_PROVIDER || 'deepseek' } });
24
+
25
+ tools.push({ name: 'precis_query', description: 'Full 12-step Precis RAG pipeline via Python bridge (no start.bat needed). Returns cited answer with VeriScore quality evaluation.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, search_mode: { type: 'string', enum: ['fast','standard','thorough'], default: 'standard' }, source_filter: { type: 'array', items: { type: 'string' } } }, required: ['query'] } });
26
+ toolHandlers.set('precis_query', async (args: unknown) => {
27
+ rateLimiter.check('precis_query', 'costly');
28
+ const { query, search_mode, source_filter } = (args ?? {}) as any;
29
+ const q = sanitizeString(query, 4000);
30
+ if (!q) throw new ValidationError('query', 'Query required.');
31
+ try { const r = await svc.call('query', { query: q, search_mode: search_mode || 'standard', source_filter }); return { content: [{ type: 'text' as const, text: JSON.stringify(r) }] }; }
32
+ catch (e: any) { throw new MCPToolError('PRECIS_ERROR', e.message); }
33
+ });
34
+
35
+ tools.push({ name: 'precis_list_documents', description: 'List indexed documents (hash + vector).', inputSchema: { type: 'object', properties: {} } });
36
+ toolHandlers.set('precis_list_documents', async () => {
37
+ rateLimiter.check('precis_list_documents', 'read');
38
+ try { const r = await svc.call('list_documents', {}); return { content: [{ type: 'text' as const, text: JSON.stringify(r) }] }; }
39
+ catch (e: any) { throw new MCPToolError('PRECIS_ERROR', e.message); }
40
+ });
41
+
42
+ tools.push({ name: 'precis_debug_stem', description: 'Show PrecisStemmer output.', inputSchema: { type: 'object', properties: { q: { type: 'string' } }, required: ['q'] } });
43
+ toolHandlers.set('precis_debug_stem', async (args: unknown) => {
44
+ try { const r = await svc.call('debug_stem', { q: (args as any).q }); return { content: [{ type: 'text' as const, text: JSON.stringify(r) }] }; }
45
+ catch (e: any) { throw new MCPToolError('PRECIS_ERROR', e.message); }
46
+ });
47
+
48
+ tools.push({ name: 'precis_debug_search', description: 'Direct hybrid search.', inputSchema: { type: 'object', properties: { q: { type: 'string' } }, required: ['q'] } });
49
+ toolHandlers.set('precis_debug_search', async (args: unknown) => {
50
+ try { const r = await svc.call('debug_search', { q: (args as any).q }); return { content: [{ type: 'text' as const, text: JSON.stringify(r) }] }; }
51
+ catch (e: any) { throw new MCPToolError('PRECIS_ERROR', e.message); }
52
+ });
53
+
54
+ for (const [name, desc] of [['precis_upload_document','Upload document'],['precis_upload_batch','Batch upload'],['precis_extract_work_order','Extract work order'],['precis_list_work_orders','List work orders']] as [string,string][]) {
55
+ tools.push({ name, description: desc + ' (backend initializes on first call)', inputSchema: { type: 'object', properties: { data: { type: 'string' }, filename: { type: 'string' } } } });
56
+ toolHandlers.set(name, async () => ({ content: [{ type: 'text' as const, text: JSON.stringify({ note: name + ': bridge initializes on first request. Try again.' }) }] }));
57
+ }
58
+
59
+ resources.push({ uri: 'precis://documents/{filename}', name: 'Document', description: 'Indexed doc.', mimeType: 'application/json' });
60
+ resources.push({ uri: 'precis://traces/{query_id}', name: 'Trace', description: 'Audit trace.', mimeType: 'application/json' });
61
+ resources.push({ uri: 'precis://evaluations/{query_id}', name: 'VeriScore', description: 'Quality eval.', mimeType: 'application/json' });
62
+ prompts.push({ name: 'precis/rag-query', description: 'RAG template.', arguments: [{ name: 'question', description: 'The question to answer via RAG', required: true }] });
63
+ promptHandlers.set('precis/rag-query', async (a) => ({ messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'Answer from docs: ' + (a?.question || '') } }] }));
64
+ prompts.push({ name: 'precis/work-order-extraction', description: 'Work order template.', arguments: [] });
65
+ promptHandlers.set('precis/work-order-extraction', async () => ({ messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'Extract work order fields.' } }] }));
66
+ logger.info('Precis: 8 tools via Python bridge');
67
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "composite": true
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["dist", "__tests__"],
10
+ "references": [
11
+ { "path": "../core" }
12
+ ]
13
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@unified-mcp/server",
3
+ "version": "0.1.0",
4
+ "description": "Unified MCP Server — main entry point registering all tools, resources, and prompts",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "bin": {
9
+ "unified-jk-mcp": "./dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "typecheck": "tsc -p tsconfig.json --noEmit",
14
+ "start": "node dist/index.js",
15
+ "dev": "node --import tsx src/index.ts"
16
+ },
17
+ "dependencies": {
18
+ "@unified-mcp/core": "^0.1.0",
19
+ "@unified-mcp/deeppipe": "^0.1.0",
20
+ "@unified-mcp/piste": "^0.1.0",
21
+ "@unified-mcp/precis": "^0.1.0",
22
+ "@unified-mcp/clinical": "^0.1.0",
23
+ "@modelcontextprotocol/sdk": "^1.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "tsx": "^4.0.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ }
31
+ }