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.
- package/.env.example +93 -0
- package/README.md +258 -0
- package/package.json +70 -0
- package/packages/clinical/package.json +22 -0
- package/packages/clinical/src/index.ts +262 -0
- package/packages/clinical/tsconfig.json +13 -0
- package/packages/core/package.json +21 -0
- package/packages/core/src/config.ts +138 -0
- package/packages/core/src/errors.ts +100 -0
- package/packages/core/src/index.ts +104 -0
- package/packages/core/src/llm-config.ts +213 -0
- package/packages/core/src/logging.ts +66 -0
- package/packages/core/src/python-bridge.ts +384 -0
- package/packages/core/src/rate-limiter.ts +136 -0
- package/packages/core/src/types.ts +203 -0
- package/packages/core/src/validation.ts +101 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/deeppipe/package.json +21 -0
- package/packages/deeppipe/src/index.ts +424 -0
- package/packages/deeppipe/tsconfig.json +13 -0
- package/packages/piste/package.json +20 -0
- package/packages/piste/src/index.ts +48 -0
- package/packages/piste/tsconfig.json +13 -0
- package/packages/precis/package.json +20 -0
- package/packages/precis/src/index.ts +67 -0
- package/packages/precis/tsconfig.json +13 -0
- package/packages/server/package.json +31 -0
- package/packages/server/src/index.ts +427 -0
- package/packages/server/tsconfig.json +17 -0
- package/setup.mjs +141 -0
- package/test.mjs +337 -0
- package/vendors/clinical-intake/pipeline.mjs +349 -0
- package/vendors/clinical-intake/questions/en.txt +9 -0
- package/vendors/clinical-intake/questions/fr.txt +9 -0
- package/vendors/piste/.env.example +73 -0
- package/vendors/piste/app/core/__init__.py +4 -0
- package/vendors/piste/app/core/config.py +83 -0
- package/vendors/piste/app/core/debuglog.py +16 -0
- package/vendors/piste/app/core/middleware.py +40 -0
- package/vendors/piste/bridge_piste.py +301 -0
- package/vendors/piste/pipeline/__init__.py +4 -0
- package/vendors/piste/pipeline/compiler.py +68 -0
- package/vendors/piste/pipeline/offline/__init__.py +28 -0
- package/vendors/piste/pipeline/offline/verifaid_pipeline.py +247 -0
- package/vendors/piste/pipeline/replay.py +15 -0
- package/vendors/piste/pipeline/replay_engine.py +249 -0
- package/vendors/piste/pipeline/signatures/__init__.py +4 -0
- package/vendors/piste/pipeline/signatures/signatures.py +136 -0
- package/vendors/piste/pipeline/stage1/__init__.py +21 -0
- package/vendors/piste/pipeline/stage1/atomic_decomposer.py +61 -0
- package/vendors/piste/pipeline/stage1/check_worthiness.py +100 -0
- package/vendors/piste/pipeline/stage1/orchestrator.py +175 -0
- package/vendors/piste/pipeline/stage1/test_stage1.py +162 -0
- package/vendors/piste/pipeline/stage2/__init__.py +34 -0
- package/vendors/piste/pipeline/stage2/blind_retriever.py +303 -0
- package/vendors/piste/pipeline/stage2/canonical_mapper.py +124 -0
- package/vendors/piste/pipeline/stage2/credibility_scorer.py +85 -0
- package/vendors/piste/pipeline/stage2/orchestrator.py +311 -0
- package/vendors/piste/pipeline/stage2/query_refiner.py +88 -0
- package/vendors/piste/pipeline/stage2/search_decision.py +69 -0
- package/vendors/piste/pipeline/stage2/test_stage2.py +265 -0
- package/vendors/piste/pipeline/stage3/__init__.py +20 -0
- package/vendors/piste/pipeline/stage3/classifier.py +79 -0
- package/vendors/piste/pipeline/stage3/orchestrator.py +225 -0
- package/vendors/piste/pipeline/stage3/test_stage3.py +101 -0
- package/vendors/piste/pipeline/stage4/__init__.py +33 -0
- package/vendors/piste/pipeline/stage4/criticality_gate.py +177 -0
- package/vendors/piste/pipeline/stage4/orchestrator.py +269 -0
- package/vendors/piste/pipeline/stage4/test_stage4.py +192 -0
- package/vendors/piste/pipeline/stage4/verdict_aggregator.py +157 -0
- package/vendors/piste/requirements.txt +53 -0
- package/vendors/precis/backend/__init__.py +6 -0
- package/vendors/precis/backend/agents/__init__.py +3 -0
- package/vendors/precis/backend/agents/data_synthesis.py +105 -0
- package/vendors/precis/backend/agents/dist_free_synth.py +97 -0
- package/vendors/precis/backend/agents/exact_hash_retriever.py +327 -0
- package/vendors/precis/backend/agents/fusion_ranker.py +64 -0
- package/vendors/precis/backend/agents/guardrail.py +175 -0
- package/vendors/precis/backend/agents/query_expander.py +89 -0
- package/vendors/precis/backend/agents/radial_interpol.py +99 -0
- package/vendors/precis/backend/agents/report_generator.py +92 -0
- package/vendors/precis/backend/agents/semantic_reranker.py +135 -0
- package/vendors/precis/backend/agents/stat_anomaly.py +93 -0
- package/vendors/precis/backend/agents/vector_index.py +123 -0
- package/vendors/precis/backend/agents/veri_score.py +341 -0
- package/vendors/precis/backend/agents/work_order_extractor.py +205 -0
- package/vendors/precis/backend/api/__init__.py +3 -0
- package/vendors/precis/backend/api/routes/__init__.py +3 -0
- package/vendors/precis/backend/config.py +88 -0
- package/vendors/precis/backend/core/__init__.py +13 -0
- package/vendors/precis/backend/core/hashing.py +22 -0
- package/vendors/precis/backend/core/metrics.py +77 -0
- package/vendors/precis/backend/core/multitoken.py +166 -0
- package/vendors/precis/backend/core/pmi.py +54 -0
- package/vendors/precis/backend/core/stemming.py +74 -0
- package/vendors/precis/backend/core/tracing.py +150 -0
- package/vendors/precis/backend/data/__init__.py +3 -0
- package/vendors/precis/backend/data/chunker.py +57 -0
- package/vendors/precis/backend/data/pdf_parser.py +42 -0
- package/vendors/precis/backend/db/__init__.py +3 -0
- package/vendors/precis/backend/db/models.py +173 -0
- package/vendors/precis/backend/db/repository.py +269 -0
- package/vendors/precis/backend/llm/__init__.py +3 -0
- package/vendors/precis/backend/llm/anthropic_provider.py +39 -0
- package/vendors/precis/backend/llm/base.py +147 -0
- package/vendors/precis/backend/llm/deepseek_provider.py +43 -0
- package/vendors/precis/backend/llm/factory.py +60 -0
- package/vendors/precis/backend/llm/google_provider.py +39 -0
- package/vendors/precis/backend/llm/ollama_provider.py +54 -0
- package/vendors/precis/backend/llm/openai_provider.py +50 -0
- package/vendors/precis/backend/main.py +677 -0
- package/vendors/precis/backend/orchestrator/__init__.py +3 -0
- package/vendors/precis/backend/orchestrator/planner.py +81 -0
- package/vendors/precis/backend/orchestrator/router.py +319 -0
- package/vendors/precis/backend/orchestrator/types.py +58 -0
- package/vendors/precis/bridge_precis.py +185 -0
- package/vendors/precis/data/sample_reports/README.md +8 -0
- package/vendors/precis/data/seed_data.py +115 -0
- package/vendors/precis/requirements.txt +19 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clinical Intake Integration for Unified MCP Server
|
|
3
|
+
*
|
|
4
|
+
* USES THE REAL clinical-intake/pipeline.mjs — NOT A REWRITE.
|
|
5
|
+
* Imports directly from the cloned clinical-intake repo.
|
|
6
|
+
*
|
|
7
|
+
* Multi-provider LLM via the real pipeline's clinicalChat().
|
|
8
|
+
* STT: Groq (whisper-large-v3) via the real pipeline
|
|
9
|
+
* TTS: ElevenLabs via the real pipeline
|
|
10
|
+
*
|
|
11
|
+
* Privacy: Anonymous by design.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
import type { Config, Logger, RateLimiter, ToolDefinition, ResourceDefinition, PromptDefinition } from '@unified-mcp/core';
|
|
18
|
+
import { ValidationError, validateBase64 } from '@unified-mcp/core';
|
|
19
|
+
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
21
|
+
// Dynamic import of the REAL clinical-intake pipeline
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
const CLINICAL_PIPELINE_PATH = '../../../vendors/clinical-intake/pipeline.mjs';
|
|
25
|
+
let _pipelineModule: any = null;
|
|
26
|
+
async function getPipeline() {
|
|
27
|
+
if (!_pipelineModule) { _pipelineModule = await import(CLINICAL_PIPELINE_PATH); }
|
|
28
|
+
return _pipelineModule;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
32
|
+
// Types
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
export interface RegisterContext {
|
|
36
|
+
config: Config;
|
|
37
|
+
logger: Logger;
|
|
38
|
+
rateLimiter: RateLimiter;
|
|
39
|
+
tools: ToolDefinition[];
|
|
40
|
+
resources: ResourceDefinition[];
|
|
41
|
+
prompts: PromptDefinition[];
|
|
42
|
+
toolHandlers: Map<string, (args: unknown) => Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }>>;
|
|
43
|
+
resourceHandlers: Map<string, (uri: string) => Promise<{ contents: Array<{ uri: string; mimeType: string; text?: string }> }>>;
|
|
44
|
+
promptHandlers: Map<string, (args?: Record<string, string>) => Promise<{ messages: Array<{ role: 'user' | 'assistant'; content: { type: 'text'; text: string } }> }>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
48
|
+
// Tool Schemas
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
const SESSION_START_SCHEMA = {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
patient_name: { type: 'string', description: 'Patient first name (not persisted).', maxLength: 100 },
|
|
55
|
+
lang: { type: 'string', enum: ['en', 'fr'], default: 'fr', description: 'Interview language.' },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const PROCESS_AUDIO_SCHEMA = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
session_id: { type: 'string', description: 'Session ID from clinical_start_session.' },
|
|
63
|
+
audio_data: { type: 'string', description: 'Base64-encoded audio (webm, mp3, wav).' },
|
|
64
|
+
},
|
|
65
|
+
required: ['session_id', 'audio_data'],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const GENERATE_PODCAST_SCHEMA = {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
session_id: { type: 'string', description: 'Session ID to compile into podcast MP3.' },
|
|
72
|
+
},
|
|
73
|
+
required: ['session_id'],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const LIST_SESSIONS_SCHEMA = { type: 'object', properties: {} };
|
|
77
|
+
|
|
78
|
+
const GET_SESSION_SCHEMA = {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
session_id: { type: 'string', description: 'Session ID to retrieve.' },
|
|
82
|
+
},
|
|
83
|
+
required: ['session_id'],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
87
|
+
// Registration — ALL logic from clinical-intake/pipeline.mjs
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
89
|
+
|
|
90
|
+
export function registerClinical(ctx: RegisterContext): void {
|
|
91
|
+
const { config, logger, rateLimiter, tools, resources, prompts, toolHandlers, resourceHandlers, promptHandlers } = ctx;
|
|
92
|
+
|
|
93
|
+
const llmConfig = config.clinicalLLM;
|
|
94
|
+
logger.info(`Clinical LLM: provider=${llmConfig.provider}, model=${llmConfig.model}`);
|
|
95
|
+
logger.info('Using REAL pipeline from clinical-intake/pipeline.mjs');
|
|
96
|
+
|
|
97
|
+
// ── clinical_start_session ─────────────────────────────────────
|
|
98
|
+
tools.push({
|
|
99
|
+
name: 'clinical_start_session',
|
|
100
|
+
description: 'Start a new anonymous clinical intake session. Uses the real clinical-intake pipeline: bilingual questionnaires (EN: Health Canada, FR: RAMQ), Groq STT, multi-provider LLM, ElevenLabs TTS. Returns session ID, greeting text, and greeting audio.',
|
|
101
|
+
inputSchema: SESSION_START_SCHEMA,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
toolHandlers.set('clinical_start_session', async (args: unknown) => {
|
|
105
|
+
rateLimiter.check('clinical_start_session', 'costly');
|
|
106
|
+
const pipe = await getPipeline();
|
|
107
|
+
|
|
108
|
+
const { patient_name, lang } = (args ?? {}) as any;
|
|
109
|
+
const language = lang === 'en' ? 'en' : 'fr';
|
|
110
|
+
const sid = pipe.generateSessionId();
|
|
111
|
+
pipe.getSession(sid, patient_name || '', language);
|
|
112
|
+
const session = pipe.getSession(sid);
|
|
113
|
+
|
|
114
|
+
let greetingAudioBase64: string | null = null;
|
|
115
|
+
try {
|
|
116
|
+
const audioBuffer = await pipe.synthesize(session.primingText, config.ELEVENLABS_API_KEY, config.ELEVENLABS_VOICE_ID);
|
|
117
|
+
greetingAudioBase64 = audioBuffer.toString('base64');
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
logger.warn(`TTS unavailable: ${err.message}`, 'clinical_start_session');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.info(`Clinical session: ${sid} (${language})`, 'clinical_start_session');
|
|
123
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({
|
|
124
|
+
session_id: sid, greeting_text: session.primingText, greeting_audio_base64: greetingAudioBase64,
|
|
125
|
+
lang: session.lang, total_questions: session.questions.length,
|
|
126
|
+
_llm: { provider: llmConfig.provider, model: llmConfig.model, configured: !!llmConfig.apiKey },
|
|
127
|
+
}) }] };
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── clinical_process_audio ─────────────────────────────────────
|
|
131
|
+
tools.push({
|
|
132
|
+
name: 'clinical_process_audio',
|
|
133
|
+
description: 'Process patient audio through the REAL clinical pipeline: Groq STT transcription → Multi-provider LLM clinical reasoning → ElevenLabs TTS voice reply. Returns transcript, LLM response, and audio reply.',
|
|
134
|
+
inputSchema: PROCESS_AUDIO_SCHEMA,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
toolHandlers.set('clinical_process_audio', async (args: unknown) => {
|
|
138
|
+
rateLimiter.check('clinical_process_audio', 'costly');
|
|
139
|
+
const pipe = await getPipeline();
|
|
140
|
+
|
|
141
|
+
const { session_id, audio_data } = (args ?? {}) as any;
|
|
142
|
+
const validation = validateBase64(audio_data, 10 * 1024 * 1024);
|
|
143
|
+
if (!validation.valid) throw new ValidationError('audio_data', validation.error);
|
|
144
|
+
|
|
145
|
+
const session = pipe.getSession(session_id);
|
|
146
|
+
if (!session?.primingText) throw new ValidationError('session_id', `Session "${session_id}" not found.`);
|
|
147
|
+
|
|
148
|
+
const result = await pipe.processClinicalTurn(session_id, validation.buffer, {
|
|
149
|
+
groqApiKey: config.GROQ_API_KEY,
|
|
150
|
+
llmApiKey: llmConfig.apiKey,
|
|
151
|
+
llmBaseUrl: llmConfig.baseUrl,
|
|
152
|
+
llmModel: llmConfig.model,
|
|
153
|
+
elevenlabsApiKey: config.ELEVENLABS_API_KEY,
|
|
154
|
+
elevenlabsVoiceId: config.ELEVENLABS_VOICE_ID,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
logger.info(`Turn ${result.turn_number}: "${result.user_text.slice(0, 60)}..."`, 'clinical_process_audio');
|
|
158
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] };
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── clinical_generate_podcast ──────────────────────────────────
|
|
162
|
+
tools.push({
|
|
163
|
+
name: 'clinical_generate_podcast',
|
|
164
|
+
description: 'Compile a complete clinical encounter into an MP3 podcast using the REAL pipeline. All dialog turns + greeting combined.',
|
|
165
|
+
inputSchema: GENERATE_PODCAST_SCHEMA,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
toolHandlers.set('clinical_generate_podcast', async (args: unknown) => {
|
|
169
|
+
rateLimiter.check('clinical_generate_podcast', 'costly');
|
|
170
|
+
const pipe = await getPipeline();
|
|
171
|
+
|
|
172
|
+
const { session_id } = (args ?? {}) as any;
|
|
173
|
+
const session = pipe.getSession(session_id);
|
|
174
|
+
if (!session?.primingText) throw new ValidationError('session_id', `Session "${session_id}" not found.`);
|
|
175
|
+
if (session.dialog.length === 0) {
|
|
176
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'No conversation to render.' }) }], isError: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const podcast = await pipe.buildPodcast(session_id, {
|
|
180
|
+
elevenlabsApiKey: config.ELEVENLABS_API_KEY,
|
|
181
|
+
elevenlabsVoiceId: config.ELEVENLABS_VOICE_ID,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
logger.info(`Podcast: ${session_id} (${(podcast.length / 1024).toFixed(1)} KB)`, 'clinical_generate_podcast');
|
|
185
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({
|
|
186
|
+
podcast_base64: podcast.toString('base64'), session_id, turns: session.dialog.length + 1,
|
|
187
|
+
duration_seconds: Math.round(session.dialog.length * 15), file_size_bytes: podcast.length,
|
|
188
|
+
patient_name: session.patientName || '(anonymous)', lang: session.lang,
|
|
189
|
+
}) }] };
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── clinical_list_sessions ─────────────────────────────────────
|
|
193
|
+
tools.push({
|
|
194
|
+
name: 'clinical_list_sessions',
|
|
195
|
+
description: 'List all clinical intake sessions from the real pipeline. Anonymous by design.',
|
|
196
|
+
inputSchema: LIST_SESSIONS_SCHEMA,
|
|
197
|
+
});
|
|
198
|
+
toolHandlers.set('clinical_list_sessions', async () => {
|
|
199
|
+
rateLimiter.check('clinical_list_sessions', 'read');
|
|
200
|
+
const pipe = await getPipeline();
|
|
201
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({ sessions: pipe.listSessions() }) }] };
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── clinical_get_session ───────────────────────────────────────
|
|
205
|
+
tools.push({
|
|
206
|
+
name: 'clinical_get_session',
|
|
207
|
+
description: 'Get full session detail with dialog turns (text only, audio excluded).',
|
|
208
|
+
inputSchema: GET_SESSION_SCHEMA,
|
|
209
|
+
});
|
|
210
|
+
toolHandlers.set('clinical_get_session', async (args: unknown) => {
|
|
211
|
+
rateLimiter.check('clinical_get_session', 'read');
|
|
212
|
+
const pipe = await getPipeline();
|
|
213
|
+
const { session_id } = (args ?? {}) as any;
|
|
214
|
+
const session = pipe.getSession(session_id);
|
|
215
|
+
if (!session?.primingText) throw new ValidationError('session_id', `Session "${session_id}" not found.`);
|
|
216
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify({
|
|
217
|
+
session_id, lang: session.lang,
|
|
218
|
+
turns: session.dialog.map((t: any) => ({ role: t.role, text: t.text })),
|
|
219
|
+
is_complete: session.isComplete, questions_total: session.questions.length, questions_asked: session.questionIndex,
|
|
220
|
+
created_at: session.createdAt,
|
|
221
|
+
}) }] };
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── Resources ──────────────────────────────────────────────────
|
|
225
|
+
resources.push({
|
|
226
|
+
uri: 'clinical://questions/{lang}',
|
|
227
|
+
name: 'Clinical Questionnaire',
|
|
228
|
+
description: 'Clinical intake questions from the real pipeline (clinical-intake/questions/).',
|
|
229
|
+
mimeType: 'application/json',
|
|
230
|
+
});
|
|
231
|
+
resourceHandlers.set('clinical://questions/{lang}', async (uri: string) => {
|
|
232
|
+
const pipe = await getPipeline();
|
|
233
|
+
const match = uri.match(/^clinical:\/\/questions\/(en|fr)$/);
|
|
234
|
+
if (!match) throw new ValidationError('uri', `Invalid: ${uri}`);
|
|
235
|
+
const lang = match[1] as 'en' | 'fr';
|
|
236
|
+
const questions = lang === 'fr' ? pipe.QUESTIONS_FR : pipe.QUESTIONS_EN;
|
|
237
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify({ lang, questions, count: questions.length }) }] };
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── Prompts ────────────────────────────────────────────────────
|
|
241
|
+
prompts.push({
|
|
242
|
+
name: 'clinical/intake-en',
|
|
243
|
+
description: 'English clinical intake prompt from the real pipeline. Includes all 8 Health Canada questions and clinical safety rules.',
|
|
244
|
+
arguments: [{ name: 'patient_name', description: 'Patient first name.', required: false }],
|
|
245
|
+
});
|
|
246
|
+
promptHandlers.set('clinical/intake-en', async (args?: Record<string, string>) => {
|
|
247
|
+
const pipe = await getPipeline();
|
|
248
|
+
return { messages: [{ role: 'user' as const, content: { type: 'text' as const, text: pipe.buildSystemPrompt('en', args?.patient_name || 'sir/madam') } }] };
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
prompts.push({
|
|
252
|
+
name: 'clinical/intake-fr',
|
|
253
|
+
description: 'French clinical intake prompt from the real pipeline (RAMQ). Includes all 8 questions and clinical safety rules.',
|
|
254
|
+
arguments: [{ name: 'patient_name', description: 'Patient first name.', required: false }],
|
|
255
|
+
});
|
|
256
|
+
promptHandlers.set('clinical/intake-fr', async (args?: Record<string, string>) => {
|
|
257
|
+
const pipe = await getPipeline();
|
|
258
|
+
return { messages: [{ role: 'user' as const, content: { type: 'text' as const, text: pipe.buildSystemPrompt('fr', args?.patient_name || 'M./Mme') } }] };
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
logger.info('Clinical: 5 tools, 1 resource, 2 prompts — using REAL clinical-intake/pipeline.mjs');
|
|
262
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unified-mcp/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared utilities, types, config, and multi-provider LLM abstraction for the unified MCP server",
|
|
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
|
+
"dotenv": "^16.4.5",
|
|
16
|
+
"zod": "^3.23.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"vitest": "^2.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Configuration Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads and validates all environment variables using Zod schemas.
|
|
5
|
+
* Re-exports the resolved LLM config for convenience.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import dotenv from 'dotenv';
|
|
10
|
+
import { resolveLLMConfig, llmProviderSchema, type ResolvedLLMConfig } from './llm-config.js';
|
|
11
|
+
|
|
12
|
+
dotenv.config();
|
|
13
|
+
|
|
14
|
+
// ── Full configuration schema ────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export const configSchema = z.object({
|
|
17
|
+
// --- LLM Defaults ---
|
|
18
|
+
LLM_DEFAULT_PROVIDER: z.string().default('openai'),
|
|
19
|
+
LLM_DEFAULT_API_KEY: z.string().default(''),
|
|
20
|
+
LLM_DEFAULT_BASE_URL: z.string().default(''),
|
|
21
|
+
LLM_DEFAULT_MODEL: z.string().default(''),
|
|
22
|
+
|
|
23
|
+
// --- Azure ---
|
|
24
|
+
AZURE_OPENAI_ENDPOINT: z.string().default(''),
|
|
25
|
+
AZURE_OPENAI_API_KEY: z.string().default(''),
|
|
26
|
+
AZURE_OPENAI_DEPLOYMENT: z.string().default(''),
|
|
27
|
+
AZURE_OPENAI_API_VERSION: z.string().default('2024-08-01-preview'),
|
|
28
|
+
|
|
29
|
+
// --- Anthropic ---
|
|
30
|
+
ANTHROPIC_API_KEY: z.string().default(''),
|
|
31
|
+
ANTHROPIC_BASE_URL: z.string().default(''),
|
|
32
|
+
|
|
33
|
+
// --- Google ---
|
|
34
|
+
GOOGLE_API_KEY: z.string().default(''),
|
|
35
|
+
|
|
36
|
+
// --- Ollama ---
|
|
37
|
+
OLLAMA_HOST: z.string().default('http://localhost:11434'),
|
|
38
|
+
|
|
39
|
+
// --- DeepPipe ---
|
|
40
|
+
DEEPPIPE_INDEX_PATH: z.string().default('./data/deeppipe.db'),
|
|
41
|
+
DEEPPIPE_LLM_PROVIDER: z.string().default(''),
|
|
42
|
+
DEEPPIPE_LLM_API_KEY: z.string().default(''),
|
|
43
|
+
DEEPPIPE_LLM_MODEL: z.string().default(''),
|
|
44
|
+
|
|
45
|
+
// --- Piste ---
|
|
46
|
+
PISTE_API_URL: z.string().default('http://localhost:8000'),
|
|
47
|
+
PISTE_LLM_PROVIDER: z.string().default(''),
|
|
48
|
+
PISTE_LLM_API_KEY: z.string().default(''),
|
|
49
|
+
TAVILY_API_KEY: z.string().default(''),
|
|
50
|
+
SERPER_API_KEY: z.string().default(''),
|
|
51
|
+
GOOGLE_CSE_API_KEY: z.string().default(''),
|
|
52
|
+
GOOGLE_CSE_ID: z.string().default(''),
|
|
53
|
+
|
|
54
|
+
// --- Precis ---
|
|
55
|
+
PRECIS_API_URL: z.string().default('http://localhost:8001'),
|
|
56
|
+
PRECIS_LLM_PROVIDER: z.string().default(''),
|
|
57
|
+
PRECIS_LLM_API_KEY: z.string().default(''),
|
|
58
|
+
|
|
59
|
+
// --- Clinical ---
|
|
60
|
+
CLINICAL_LLM_PROVIDER: z.string().default(''),
|
|
61
|
+
CLINICAL_LLM_API_KEY: z.string().default(''),
|
|
62
|
+
CLINICAL_LLM_MODEL: z.string().default(''),
|
|
63
|
+
CLINICAL_STT_PROVIDER: z.string().default('groq'),
|
|
64
|
+
GROQ_API_KEY: z.string().default(''),
|
|
65
|
+
CLINICAL_TTS_PROVIDER: z.string().default('elevenlabs'),
|
|
66
|
+
ELEVENLABS_API_KEY: z.string().default(''),
|
|
67
|
+
ELEVENLABS_VOICE_ID: z.string().default('21m00Tcm4TlvDq8ikWAM'),
|
|
68
|
+
|
|
69
|
+
// --- MCP Server ---
|
|
70
|
+
MCP_SERVER_NAME: z.string().default('mcp-agentic-pipelines'),
|
|
71
|
+
MCP_SERVER_VERSION: z.string().default('1.0.0'),
|
|
72
|
+
MCP_TRANSPORT: z.enum(['stdio', 'sse']).default('stdio'),
|
|
73
|
+
MCP_SSE_PORT: z.coerce.number().int().positive().default(3100),
|
|
74
|
+
MCP_LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
75
|
+
MCP_RATE_LIMIT_ENABLED: z.enum(['true', 'false', '1', '0']).default('false').transform(v => v === 'true' || v === '1'),
|
|
76
|
+
MCP_RATE_LIMIT_MAX_RPS: z.coerce.number().int().positive().default(10),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export type RawConfig = z.infer<typeof configSchema>;
|
|
80
|
+
|
|
81
|
+
// ── Enriched config with resolved LLM settings ───────────────────────
|
|
82
|
+
|
|
83
|
+
export interface Config extends RawConfig {
|
|
84
|
+
/** Resolved LLM config for DeepPipe (chat feature). */
|
|
85
|
+
deepPipeLLM: ResolvedLLMConfig;
|
|
86
|
+
/** Resolved LLM config for Piste. */
|
|
87
|
+
pisteLLM: ResolvedLLMConfig;
|
|
88
|
+
/** Resolved LLM config for Precis. */
|
|
89
|
+
precisLLM: ResolvedLLMConfig;
|
|
90
|
+
/** Resolved LLM config for Clinical Intake. */
|
|
91
|
+
clinicalLLM: ResolvedLLMConfig;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Singleton config instance, loaded once at startup. */
|
|
95
|
+
let _config: Config | null = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load and validate configuration from environment variables.
|
|
99
|
+
* Safe to call multiple times — returns cached instance after first call.
|
|
100
|
+
*/
|
|
101
|
+
export function loadConfig(): Config {
|
|
102
|
+
if (_config) return _config;
|
|
103
|
+
|
|
104
|
+
const raw = configSchema.parse(process.env);
|
|
105
|
+
|
|
106
|
+
_config = {
|
|
107
|
+
...raw,
|
|
108
|
+
deepPipeLLM: resolveLLMConfig({
|
|
109
|
+
provider: raw.DEEPPIPE_LLM_PROVIDER || undefined,
|
|
110
|
+
apiKey: raw.DEEPPIPE_LLM_API_KEY || undefined,
|
|
111
|
+
model: raw.DEEPPIPE_LLM_MODEL || undefined,
|
|
112
|
+
}),
|
|
113
|
+
pisteLLM: resolveLLMConfig({
|
|
114
|
+
provider: raw.PISTE_LLM_PROVIDER || undefined,
|
|
115
|
+
apiKey: raw.PISTE_LLM_API_KEY || undefined,
|
|
116
|
+
}),
|
|
117
|
+
precisLLM: resolveLLMConfig({
|
|
118
|
+
provider: raw.PRECIS_LLM_PROVIDER || undefined,
|
|
119
|
+
apiKey: raw.PRECIS_LLM_API_KEY || undefined,
|
|
120
|
+
}),
|
|
121
|
+
clinicalLLM: resolveLLMConfig({
|
|
122
|
+
provider: raw.CLINICAL_LLM_PROVIDER || undefined,
|
|
123
|
+
apiKey: raw.CLINICAL_LLM_API_KEY || undefined,
|
|
124
|
+
model: raw.CLINICAL_LLM_MODEL || undefined,
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return _config;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Reset cached config (useful in tests).
|
|
133
|
+
*/
|
|
134
|
+
export function resetConfig(): void {
|
|
135
|
+
_config = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { resolveLLMConfig, type ResolvedLLMConfig } from './llm-config.js';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Error Types
|
|
3
|
+
*
|
|
4
|
+
* MCPToolError maps directly to MCP protocol error responses.
|
|
5
|
+
* All tool handlers should throw or return these.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Base error class for all MCP tool errors. */
|
|
9
|
+
export class MCPToolError extends Error {
|
|
10
|
+
public readonly name = 'MCPToolError';
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
public readonly code: string,
|
|
14
|
+
message: string,
|
|
15
|
+
public readonly statusCode: number = 400,
|
|
16
|
+
) {
|
|
17
|
+
super(message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Convert to MCP protocol error response shape. */
|
|
21
|
+
toMCPResponse(): { content: Array<{ type: 'text'; text: string }>; isError: true } {
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text' as const,
|
|
25
|
+
text: JSON.stringify({ code: this.code, message: this.message }),
|
|
26
|
+
}],
|
|
27
|
+
isError: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Convert to a plain JSON-serializable error object. */
|
|
32
|
+
toJSON(): { code: string; message: string } {
|
|
33
|
+
return { code: this.code, message: this.message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Thrown when an external service (piste, precis) is unreachable. */
|
|
38
|
+
export class ServiceUnavailableError extends MCPToolError {
|
|
39
|
+
constructor(service: string) {
|
|
40
|
+
super(
|
|
41
|
+
'SERVICE_UNAVAILABLE',
|
|
42
|
+
`${service} is not reachable. Ensure the service is running and PISTE_API_URL / PRECIS_API_URL are correctly configured.`,
|
|
43
|
+
503,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Thrown when tool input fails validation. */
|
|
49
|
+
export class ValidationError extends MCPToolError {
|
|
50
|
+
constructor(field: string, message: string) {
|
|
51
|
+
super('VALIDATION_ERROR', `${field}: ${message}`, 400);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Thrown when rate limit is exceeded. */
|
|
56
|
+
export class RateLimitError extends MCPToolError {
|
|
57
|
+
constructor(toolName: string, retryAfterMs: number = 1000) {
|
|
58
|
+
super(
|
|
59
|
+
'RATE_LIMITED',
|
|
60
|
+
`Rate limit exceeded for tool "${toolName}". Retry after ${Math.ceil(retryAfterMs / 1000)}s.`,
|
|
61
|
+
429,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Thrown when an LLM provider is not configured. */
|
|
67
|
+
export class LLMNotConfiguredError extends MCPToolError {
|
|
68
|
+
constructor(component: string, provider: string) {
|
|
69
|
+
super(
|
|
70
|
+
'LLM_NOT_CONFIGURED',
|
|
71
|
+
`${component}: LLM provider "${provider}" requires an API key. Set the appropriate env variable (e.g. LLM_DEFAULT_API_KEY or ${component.toUpperCase()}_LLM_API_KEY).`,
|
|
72
|
+
400,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Thrown when authentication fails for an external API. */
|
|
78
|
+
export class AuthenticationError extends MCPToolError {
|
|
79
|
+
constructor(service: string) {
|
|
80
|
+
super(
|
|
81
|
+
'AUTHENTICATION_ERROR',
|
|
82
|
+
`${service} rejected the API key. Verify your credentials.`,
|
|
83
|
+
401,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Thrown when a requested resource is not found. */
|
|
89
|
+
export class NotFoundError extends MCPToolError {
|
|
90
|
+
constructor(resourceType: string, id: string) {
|
|
91
|
+
super('NOT_FOUND', `${resourceType} with id "${id}" not found.`, 404);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Thrown for internal/unexpected errors. */
|
|
96
|
+
export class InternalError extends MCPToolError {
|
|
97
|
+
constructor(message: string = 'An unexpected internal error occurred.') {
|
|
98
|
+
super('INTERNAL', message, 500);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unified-mcp/core — Barrel Export
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities, types, config, rate limiting, logging,
|
|
5
|
+
* and multi-provider LLM configuration for all integration packages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Config
|
|
9
|
+
export {
|
|
10
|
+
loadConfig,
|
|
11
|
+
resetConfig,
|
|
12
|
+
resolveLLMConfig,
|
|
13
|
+
configSchema,
|
|
14
|
+
type Config,
|
|
15
|
+
type RawConfig,
|
|
16
|
+
type ResolvedLLMConfig,
|
|
17
|
+
} from './config.js';
|
|
18
|
+
|
|
19
|
+
// Multi-Provider LLM
|
|
20
|
+
export {
|
|
21
|
+
LLM_PROVIDERS,
|
|
22
|
+
PROVIDER_DEFAULTS,
|
|
23
|
+
OPENAI_COMPATIBLE_PROVIDERS,
|
|
24
|
+
NATIVE_SDK_PROVIDERS,
|
|
25
|
+
resolveLLMConfig as resolveLLM,
|
|
26
|
+
listProviders,
|
|
27
|
+
isValidProvider,
|
|
28
|
+
llmProviderSchema,
|
|
29
|
+
llmConfigSchema,
|
|
30
|
+
type LLMProvider,
|
|
31
|
+
} from './llm-config.js';
|
|
32
|
+
|
|
33
|
+
// Errors
|
|
34
|
+
export {
|
|
35
|
+
MCPToolError,
|
|
36
|
+
ServiceUnavailableError,
|
|
37
|
+
ValidationError,
|
|
38
|
+
RateLimitError,
|
|
39
|
+
LLMNotConfiguredError,
|
|
40
|
+
AuthenticationError,
|
|
41
|
+
NotFoundError,
|
|
42
|
+
InternalError,
|
|
43
|
+
} from './errors.js';
|
|
44
|
+
|
|
45
|
+
// Validation
|
|
46
|
+
export {
|
|
47
|
+
validateArgs,
|
|
48
|
+
sanitizeString,
|
|
49
|
+
validateBase64,
|
|
50
|
+
clampInt,
|
|
51
|
+
intSchema,
|
|
52
|
+
stringSchema,
|
|
53
|
+
localeSchema,
|
|
54
|
+
base64Schema,
|
|
55
|
+
} from './validation.js';
|
|
56
|
+
|
|
57
|
+
// Logging
|
|
58
|
+
export {
|
|
59
|
+
Logger,
|
|
60
|
+
createLogger,
|
|
61
|
+
defaultLogger,
|
|
62
|
+
type LogLevel,
|
|
63
|
+
type LogEntry,
|
|
64
|
+
} from './logging.js';
|
|
65
|
+
|
|
66
|
+
// Rate Limiting
|
|
67
|
+
export {
|
|
68
|
+
TokenBucket,
|
|
69
|
+
RateLimiter,
|
|
70
|
+
createRateLimiter,
|
|
71
|
+
type RateCategory,
|
|
72
|
+
} from './rate-limiter.js';
|
|
73
|
+
|
|
74
|
+
// Python Bridge
|
|
75
|
+
export {
|
|
76
|
+
PythonService,
|
|
77
|
+
PythonServiceManager,
|
|
78
|
+
findPython,
|
|
79
|
+
resetPythonCache,
|
|
80
|
+
type PythonServiceOptions,
|
|
81
|
+
} from './python-bridge.js';
|
|
82
|
+
|
|
83
|
+
// Types
|
|
84
|
+
export type {
|
|
85
|
+
MCPTextContent,
|
|
86
|
+
MCPToolResponse,
|
|
87
|
+
MCPResourceContent,
|
|
88
|
+
ToolDefinition,
|
|
89
|
+
ResourceDefinition,
|
|
90
|
+
PromptDefinition,
|
|
91
|
+
ServiceHealth,
|
|
92
|
+
LLMProviderInfo,
|
|
93
|
+
AudioTurn,
|
|
94
|
+
ClinicalSession,
|
|
95
|
+
SearchHit,
|
|
96
|
+
SearchResults,
|
|
97
|
+
StoredDocument,
|
|
98
|
+
ChatSource,
|
|
99
|
+
ChatContext,
|
|
100
|
+
FactCheckVerdict,
|
|
101
|
+
VerdictLabel,
|
|
102
|
+
PrecisQueryResult,
|
|
103
|
+
WorkOrder,
|
|
104
|
+
} from './types.js';
|