@omophub/omophub-mcp 1.1.0 → 1.2.2

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 CHANGED
@@ -50,7 +50,7 @@ Claude: Found it - E11.9 (Type 2 diabetes mellitus without complications)
50
50
 
51
51
  ### 1. Get an API Key
52
52
 
53
- Sign up at [omophub.com](https://omophub.com) → create an API key in your [dashboard](https://omophub.com/dashboard/api-keys).
53
+ Sign up at [omophub.com](https://omophub.com) → create an API key in your [dashboard](https://dashboard.omophub.com/api-keys).
54
54
 
55
55
  ### 2. Add to Your AI Client
56
56
 
@@ -177,6 +177,9 @@ docker run -i -e OMOPHUB_API_KEY=oh_your_key_here omophub/omophub-mcp --transpor
177
177
  | `map_concept` | Map a concept to equivalent concepts in other vocabularies |
178
178
  | `get_hierarchy` | Navigate concept hierarchy - ancestors, descendants, or both |
179
179
  | `list_vocabularies` | List available medical vocabularies with statistics |
180
+ | `semantic_search` | Search using natural language with neural embeddings (understands clinical meaning) |
181
+ | `find_similar_concepts` | Find concepts similar to a reference concept, name, or description |
182
+ | `explore_concept` | Get concept details, hierarchy, and cross-vocabulary mappings in one call |
180
183
 
181
184
  ### Resources
182
185
 
@@ -208,6 +211,15 @@ docker run -i -e OMOPHUB_API_KEY=oh_your_key_here omophub/omophub-mcp --transpor
208
211
  **Validate a code →** `get_concept_by_code` → `map_concept`
209
212
  > "Is ICD-10 code E11.9 valid? What does it map to in SNOMED?"
210
213
 
214
+ **Semantic search →** `semantic_search`
215
+ > "Find concepts related to 'heart attack'"
216
+
217
+ **Explore a concept →** `explore_concept`
218
+ > "Give me everything about SNOMED concept 201826"
219
+
220
+ **Find similar →** `find_similar_concepts`
221
+ > "What concepts are similar to 'Type 2 diabetes mellitus'?"
222
+
211
223
  ---
212
224
 
213
225
  ## Configuration
@@ -244,7 +256,7 @@ In **HTTP mode**, the health endpoint is available at `/health` on the same port
244
256
  ```bash
245
257
  npx @omophub/omophub-mcp --transport=http --port=3100 --api-key=oh_your_key
246
258
  curl http://localhost:3100/health
247
- # → {"status":"ok","version":"1.1.0","uptime_seconds":42}
259
+ # → {"status":"ok","version":"1.2.2","uptime_seconds":42}
248
260
  ```
249
261
 
250
262
  In **stdio mode**, use `--health-port` for a standalone health endpoint:
@@ -281,8 +293,8 @@ OMOPHUB_API_KEY=oh_your_key npx tsx src/index.ts
281
293
  | Error | Solution |
282
294
  | :--- | :--- |
283
295
  | `API key required` | Set `OMOPHUB_API_KEY` in your environment or MCP config |
284
- | `Authentication failed` | API key may be invalid or expired - [generate a new one](https://omophub.com/dashboard/api-keys) |
285
- | `Rate limit exceeded` | Automatic retries are built in. For higher limits, [upgrade your plan](https://omophub.com/dashboard/billing) |
296
+ | `Authentication failed` | API key may be invalid or expired - [generate a new one](https://dashboard.omophub.com/api-keys) |
297
+ | `Rate limit exceeded` | Automatic retries are built in. For higher limits, [upgrade your plan](https://dashboard.omophub.com/billing) |
286
298
  | Tools not appearing | Restart your AI client, verify `npx @omophub/omophub-mcp` runs without errors, check config path |
287
299
 
288
300
  ---
@@ -290,7 +302,7 @@ OMOPHUB_API_KEY=oh_your_key npx tsx src/index.ts
290
302
  ## Links
291
303
 
292
304
  - [Documentation](https://docs.omophub.com)
293
- - [Get an API Key](https://omophub.com/dashboard/api-keys)
305
+ - [Get an API Key](https://dashboard.omophub.com/api-keys)
294
306
  - [Python SDK](https://github.com/OMOPHub/omophub-python)
295
307
  - [Community & Support](https://github.com/OMOPHub/omophub-mcp/issues)
296
308
 
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAcpE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CA4BxE"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiBpE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CA+BxE"}
package/dist/server.js CHANGED
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { OmopHubClient } from './client/api.js';
3
3
  import { registerPrompts } from './prompts/index.js';
4
4
  import { registerResources } from './resources/index.js';
5
- import { registerConceptTools, registerHierarchyTools, registerMappingTools, registerSearchTools, registerVocabularyTools, } from './tools/index.js';
5
+ import { registerConceptTools, registerExploreTools, registerHierarchyTools, registerMappingTools, registerSearchTools, registerSemanticSearchTools, registerSimilarTools, registerVocabularyTools, } from './tools/index.js';
6
6
  import { logger } from './utils/logger.js';
7
7
  import { VERSION } from './version.js';
8
8
  export function createServer(apiKey, baseUrl) {
@@ -12,9 +12,12 @@ export function createServer(apiKey, baseUrl) {
12
12
  description: 'Access OHDSI standardized medical vocabularies — search concepts, navigate hierarchies, and map between clinical coding systems like SNOMED CT, ICD-10, RxNorm, and LOINC.',
13
13
  });
14
14
  const client = new OmopHubClient(apiKey, baseUrl);
15
- // Register all Phase 1 tools
15
+ // Register tools
16
16
  registerSearchTools(server, client);
17
+ registerSemanticSearchTools(server, client);
18
+ registerSimilarTools(server, client);
17
19
  registerConceptTools(server, client);
20
+ registerExploreTools(server, client);
18
21
  registerMappingTools(server, client);
19
22
  registerHierarchyTools(server, client);
20
23
  registerVocabularyTools(server, client);
@@ -22,7 +25,7 @@ export function createServer(apiKey, baseUrl) {
22
25
  registerResources(server, client);
23
26
  registerPrompts(server);
24
27
  logger.info('OMOPHub MCP server initialized', {
25
- tools: 6,
28
+ tools: 9,
26
29
  resources: 2,
27
30
  prompts: 2,
28
31
  });
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,OAAgB;IAC3D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,OAAO;QAChB,WAAW,EACT,4KAA4K;KAC/K,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElD,6BAA6B;IAC7B,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC,iCAAiC;IACjC,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,eAAe,CAAC,MAAM,CAAC,CAAC;IAExB,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE;QAC5C,KAAK,EAAE,CAAC;QACR,SAAS,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;KACX,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,2BAA2B,EAC3B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,OAAgB;IAC3D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,OAAO;QAChB,WAAW,EACT,4KAA4K;KAC/K,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElD,iBAAiB;IACjB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,2BAA2B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC,iCAAiC;IACjC,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,eAAe,CAAC,MAAM,CAAC,CAAC;IAExB,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE;QAC5C,KAAK,EAAE,CAAC;QACR,SAAS,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;KACX,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { OmopHubClient } from '../client/api.js';
3
+ export declare function registerExploreTools(server: McpServer, client: OmopHubClient): void;
4
+ //# sourceMappingURL=explore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explore.d.ts","sourceRoot":"","sources":["../../src/tools/explore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAsBtD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CA0OnF"}
@@ -0,0 +1,182 @@
1
+ import { z } from 'zod';
2
+ import { formatErrorForMcp } from '../utils/errors.js';
3
+ export function registerExploreTools(server, client) {
4
+ server.tool('explore_concept', 'Get a comprehensive view of a medical concept in one call: detailed info, ancestors/descendants hierarchy, and cross-vocabulary mappings. Use this instead of calling get_concept + get_hierarchy + map_concept separately. Ideal for understanding what a concept is, where it sits in the classification tree, and how it maps to other coding systems.', {
5
+ concept_id: z.number().describe('The OMOP concept_id to explore'),
6
+ include_hierarchy: z
7
+ .boolean()
8
+ .default(true)
9
+ .describe('Include ancestors and descendants. Default true.'),
10
+ hierarchy_levels: z
11
+ .number()
12
+ .min(1)
13
+ .max(5)
14
+ .default(2)
15
+ .describe('How many hierarchy levels to fetch (1-5, default 2)'),
16
+ include_mappings: z
17
+ .boolean()
18
+ .default(true)
19
+ .describe('Include cross-vocabulary mappings. Default true.'),
20
+ target_vocabularies: z
21
+ .string()
22
+ .max(200)
23
+ .optional()
24
+ .describe("Comma-separated vocabulary IDs to filter mappings. Examples: 'ICD10CM', 'SNOMED'."),
25
+ }, async ({ concept_id, include_hierarchy, include_mappings, hierarchy_levels, target_vocabularies, }) => {
26
+ try {
27
+ // Build parallel requests
28
+ const requests = [];
29
+ // 1. Always fetch concept details
30
+ const conceptReq = client.request(`/concepts/${concept_id}`, {}, 'explore_concept');
31
+ requests.push(conceptReq);
32
+ // 2. Optionally fetch hierarchy
33
+ let hierarchyReq = null;
34
+ if (include_hierarchy !== false) {
35
+ const levels = hierarchy_levels ?? 2;
36
+ hierarchyReq = client.request(`/concepts/${concept_id}/hierarchy`, { max_levels: levels }, 'explore_concept');
37
+ requests.push(hierarchyReq);
38
+ }
39
+ // 3. Optionally fetch mappings via relationships endpoint
40
+ // (The /mappings endpoint has a schema resolution bug — using /relationships instead)
41
+ let mappingsReq = null;
42
+ if (include_mappings !== false) {
43
+ mappingsReq = client.request(`/concepts/${concept_id}/relationships`, { page_size: 100 }, 'explore_concept');
44
+ requests.push(mappingsReq);
45
+ }
46
+ // Execute all in parallel
47
+ const settled = await Promise.allSettled(requests);
48
+ // Extract results
49
+ const conceptResult = settled[0];
50
+ const concept = conceptResult.status === 'fulfilled'
51
+ ? conceptResult.value.data
52
+ : null;
53
+ let hierarchy = null;
54
+ let relationshipsData = null;
55
+ let idx = 1;
56
+ if (hierarchyReq) {
57
+ const r = settled[idx++];
58
+ if (r.status === 'fulfilled') {
59
+ hierarchy = r.value.data;
60
+ }
61
+ }
62
+ if (mappingsReq) {
63
+ const r = settled[idx];
64
+ if (r.status === 'fulfilled') {
65
+ relationshipsData = r.value.data;
66
+ }
67
+ }
68
+ if (!concept) {
69
+ // If the request was rejected, propagate the original error
70
+ if (conceptResult.status === 'rejected') {
71
+ const { text, json } = formatErrorForMcp(conceptResult.reason, 'explore_concept');
72
+ return {
73
+ content: [
74
+ { type: 'text', text },
75
+ { type: 'text', text: json },
76
+ ],
77
+ isError: true,
78
+ };
79
+ }
80
+ return {
81
+ content: [{ type: 'text', text: `Concept ${concept_id} not found.` }],
82
+ isError: true,
83
+ };
84
+ }
85
+ // Build human-readable output
86
+ const sections = [];
87
+ // Concept info
88
+ const std = concept.standard_concept === 'S'
89
+ ? ' [Standard]'
90
+ : concept.standard_concept === 'C'
91
+ ? ' [Classification]'
92
+ : '';
93
+ sections.push(`## ${concept.concept_name}${std}\n` +
94
+ `- **Concept ID:** ${concept.concept_id}\n` +
95
+ `- **Vocabulary:** ${concept.vocabulary_id}\n` +
96
+ `- **Domain:** ${concept.domain_id}\n` +
97
+ `- **Class:** ${concept.concept_class_id}\n` +
98
+ `- **Code:** ${concept.concept_code}`);
99
+ // Hierarchy
100
+ if (hierarchy) {
101
+ const ancestors = hierarchy.ancestors ?? [];
102
+ const descendants = hierarchy.descendants ?? [];
103
+ if (ancestors.length > 0 || descendants.length > 0) {
104
+ const hierarchyLines = ['## Hierarchy'];
105
+ if (ancestors.length > 0) {
106
+ hierarchyLines.push(`\n### Ancestors (${ancestors.length})`);
107
+ for (const a of ancestors.slice(0, 10)) {
108
+ const level = a.min_levels_of_separation ?? a.level ?? '';
109
+ hierarchyLines.push(` ${' '.repeat(Number(level) || 0)}↑ ${a.concept_name} (${a.vocabulary_id}, ID: ${a.concept_id})`);
110
+ }
111
+ if (ancestors.length > 10)
112
+ hierarchyLines.push(` ... and ${ancestors.length - 10} more`);
113
+ }
114
+ if (descendants.length > 0) {
115
+ hierarchyLines.push(`\n### Descendants (${descendants.length})`);
116
+ for (const d of descendants.slice(0, 10)) {
117
+ const level = d.min_levels_of_separation ?? d.level ?? '';
118
+ hierarchyLines.push(` ${' '.repeat(Number(level) || 0)}↓ ${d.concept_name} (${d.vocabulary_id}, ID: ${d.concept_id})`);
119
+ }
120
+ if (descendants.length > 10)
121
+ hierarchyLines.push(` ... and ${descendants.length - 10} more`);
122
+ }
123
+ sections.push(hierarchyLines.join('\n'));
124
+ }
125
+ }
126
+ // Mappings — extracted from relationships with "Maps to" type
127
+ if (relationshipsData) {
128
+ const allRels = relationshipsData.relationships ?? [];
129
+ // Filter for mapping relationships
130
+ const mappingRels = allRels.filter((r) => r.relationship_id === 'Maps to' || r.relationship_id === 'Mapped from');
131
+ // Optionally filter by target vocabularies
132
+ const vocabFilter = target_vocabularies
133
+ ? target_vocabularies
134
+ .split(',')
135
+ .map((v) => v.trim())
136
+ .filter(Boolean)
137
+ : null;
138
+ const filtered = vocabFilter
139
+ ? mappingRels.filter((r) => vocabFilter.includes(r.concept_2?.vocabulary_id ?? ''))
140
+ : mappingRels;
141
+ if (filtered.length > 0) {
142
+ const mapLines = [`## Cross-Vocabulary Mappings (${filtered.length})`];
143
+ for (const r of filtered.slice(0, 15)) {
144
+ const c2 = r.concept_2;
145
+ if (c2) {
146
+ mapLines.push(`- **${c2.concept_name}** → ${c2.vocabulary_id} (${c2.concept_code}) via _${r.relationship_id}_`);
147
+ }
148
+ }
149
+ if (filtered.length > 15)
150
+ mapLines.push(`... and ${filtered.length - 15} more`);
151
+ sections.push(mapLines.join('\n'));
152
+ }
153
+ else {
154
+ const isStandard = concept.standard_concept === 'S';
155
+ const hint = isStandard
156
+ ? ' Standard concepts are typically the *target* of mappings, not the source. To find what maps to this concept, try searching for a non-standard source concept (e.g., an ICD-10 code) and use explore_concept on that.'
157
+ : '';
158
+ sections.push(`## Mappings\n\nNo cross-vocabulary mappings found.${hint}`);
159
+ }
160
+ }
161
+ const text = sections.join('\n\n');
162
+ const jsonData = { concept, hierarchy, relationships: relationshipsData };
163
+ return {
164
+ content: [
165
+ { type: 'text', text },
166
+ { type: 'text', text: JSON.stringify(jsonData) },
167
+ ],
168
+ };
169
+ }
170
+ catch (error) {
171
+ const { text, json } = formatErrorForMcp(error, 'explore_concept');
172
+ return {
173
+ content: [
174
+ { type: 'text', text },
175
+ { type: 'text', text: json },
176
+ ],
177
+ isError: true,
178
+ };
179
+ }
180
+ });
181
+ }
182
+ //# sourceMappingURL=explore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explore.js","sourceRoot":"","sources":["../../src/tools/explore.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
1
  export { registerConceptTools } from './concepts.js';
2
+ export { registerExploreTools } from './explore.js';
2
3
  export { registerHierarchyTools } from './hierarchy.js';
3
4
  export { registerMappingTools } from './mappings.js';
4
5
  export { registerSearchTools } from './search.js';
6
+ export { registerSemanticSearchTools } from './semantic-search.js';
7
+ export { registerSimilarTools } from './similar.js';
5
8
  export { registerVocabularyTools } from './vocabularies.js';
6
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -1,6 +1,9 @@
1
1
  export { registerConceptTools } from './concepts.js';
2
+ export { registerExploreTools } from './explore.js';
2
3
  export { registerHierarchyTools } from './hierarchy.js';
3
4
  export { registerMappingTools } from './mappings.js';
4
5
  export { registerSearchTools } from './search.js';
6
+ export { registerSemanticSearchTools } from './semantic-search.js';
7
+ export { registerSimilarTools } from './similar.js';
5
8
  export { registerVocabularyTools } from './vocabularies.js';
6
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { OmopHubClient } from '../client/api.js';
3
+ export declare function registerSemanticSearchTools(server: McpServer, client: OmopHubClient): void;
4
+ //# sourceMappingURL=semantic-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-search.d.ts","sourceRoot":"","sources":["../../src/tools/semantic-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAStD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAyG1F"}
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod';
2
+ import { formatErrorForMcp } from '../utils/errors.js';
3
+ export function registerSemanticSearchTools(server, client) {
4
+ server.tool('semantic_search', "Search for medical concepts using natural language with neural embeddings. Unlike keyword search, semantic search understands clinical meaning — 'heart attack' finds 'Myocardial infarction', 'high blood sugar' finds 'Hyperglycemia'. Returns concepts ranked by similarity score. Use this when the user describes symptoms, conditions, or treatments in everyday language rather than exact medical terminology.", {
5
+ query: z
6
+ .string()
7
+ .trim()
8
+ .min(1)
9
+ .max(500)
10
+ .describe('Natural language description of the medical concept to find'),
11
+ vocabulary_ids: z
12
+ .string()
13
+ .max(200)
14
+ .optional()
15
+ .describe("Comma-separated vocabulary IDs to filter by. Examples: 'SNOMED', 'ICD10CM', 'RxNorm'."),
16
+ domain_ids: z
17
+ .string()
18
+ .max(200)
19
+ .optional()
20
+ .describe("Comma-separated domain IDs to filter by. Examples: 'Condition', 'Drug', 'Measurement'."),
21
+ standard_concept: z
22
+ .enum(['S', 'C'])
23
+ .optional()
24
+ .describe("Filter by standard concept status: 'S' for Standard, 'C' for Classification."),
25
+ threshold: z
26
+ .number()
27
+ .min(0)
28
+ .max(1)
29
+ .default(0.5)
30
+ .describe('Minimum similarity score (0.0-1.0). Higher = stricter matching. Default 0.5.'),
31
+ page_size: z
32
+ .number()
33
+ .min(1)
34
+ .max(50)
35
+ .default(10)
36
+ .describe('Number of results to return (1-50, default 10)'),
37
+ }, async ({ query, vocabulary_ids, domain_ids, standard_concept, threshold, page_size }) => {
38
+ try {
39
+ const params = {
40
+ query,
41
+ page_size: page_size ?? 10,
42
+ threshold: threshold ?? 0.5,
43
+ };
44
+ if (vocabulary_ids)
45
+ params.vocabulary_ids = vocabulary_ids;
46
+ if (domain_ids)
47
+ params.domain_ids = domain_ids;
48
+ if (standard_concept)
49
+ params.standard_concept = standard_concept;
50
+ const response = await client.request('/concepts/semantic-search', params, 'semantic_search');
51
+ const results = response.data?.results ?? [];
52
+ if (results.length === 0) {
53
+ return {
54
+ content: [
55
+ {
56
+ type: 'text',
57
+ text: `No semantic matches found for "${query}". Try different wording or lower the similarity threshold.`,
58
+ },
59
+ { type: 'text', text: JSON.stringify({ results: [], query, threshold }) },
60
+ ],
61
+ };
62
+ }
63
+ const lines = results.map((r, i) => {
64
+ const std = r.standard_concept === 'S'
65
+ ? ' [Standard]'
66
+ : r.standard_concept === 'C'
67
+ ? ' [Classification]'
68
+ : '';
69
+ const score = Number.isFinite(r.similarity_score) ? r.similarity_score.toFixed(2) : 'N/A';
70
+ return `${i + 1}. **${r.concept_name}** (ID: ${r.concept_id}) — similarity: ${score}\n ${r.vocabulary_id} | ${r.domain_id} | Code: ${r.concept_code}${std}`;
71
+ });
72
+ const text = `Found ${results.length} semantic matches for "${query}":\n\n${lines.join('\n\n')}`;
73
+ return {
74
+ content: [
75
+ { type: 'text', text },
76
+ { type: 'text', text: JSON.stringify({ results, query, threshold }) },
77
+ ],
78
+ };
79
+ }
80
+ catch (error) {
81
+ const { text, json } = formatErrorForMcp(error, 'semantic_search');
82
+ return {
83
+ content: [
84
+ { type: 'text', text },
85
+ { type: 'text', text: json },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ });
91
+ }
92
+ //# sourceMappingURL=semantic-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-search.js","sourceRoot":"","sources":["../../src/tools/semantic-search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { OmopHubClient } from '../client/api.js';
3
+ export declare function registerSimilarTools(server: McpServer, client: OmopHubClient): void;
4
+ //# sourceMappingURL=similar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similar.d.ts","sourceRoot":"","sources":["../../src/tools/similar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AA2BtD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAmKnF"}
@@ -0,0 +1,139 @@
1
+ import { z } from 'zod';
2
+ import { formatErrorForMcp } from '../utils/errors.js';
3
+ export function registerSimilarTools(server, client) {
4
+ server.tool('find_similar_concepts', "Find medical concepts similar to a reference concept, name, or natural language query. Supports three algorithms: 'semantic' (neural embeddings — best for meaning), 'lexical' (text matching — best for typos), 'hybrid' (combined — default). Provide exactly ONE of: concept_id, concept_name, or query. Use this to explore related concepts, find alternative codes, or build phenotype concept sets. Tip: For drug vocabularies like RxNorm, use drug class names ('ACE inhibitors', 'beta blockers', 'antihypertensives') rather than symptom descriptions ('medications for high blood pressure') — the embedding model aligns better with clinical terminology than lay language.", {
5
+ concept_id: z.number().optional().describe('Find concepts similar to this OMOP concept ID'),
6
+ concept_name: z
7
+ .string()
8
+ .max(500)
9
+ .optional()
10
+ .describe('Find concepts similar to this concept name'),
11
+ query: z
12
+ .string()
13
+ .max(500)
14
+ .optional()
15
+ .describe('Find concepts matching this natural language description'),
16
+ algorithm: z
17
+ .enum(['semantic', 'lexical', 'hybrid'])
18
+ .default('hybrid')
19
+ .describe("Similarity algorithm: 'semantic' (meaning), 'lexical' (text), 'hybrid' (both). Default 'hybrid'."),
20
+ similarity_threshold: z
21
+ .number()
22
+ .min(0)
23
+ .max(1)
24
+ .default(0.7)
25
+ .describe('Minimum similarity score (0.0-1.0). Default 0.7.'),
26
+ page_size: z
27
+ .number()
28
+ .min(1)
29
+ .max(100)
30
+ .default(20)
31
+ .describe('Number of results (1-100, default 20)'),
32
+ vocabulary_ids: z
33
+ .string()
34
+ .max(200)
35
+ .optional()
36
+ .describe("Comma-separated vocabulary IDs to filter results. Examples: 'SNOMED', 'ICD10CM'."),
37
+ domain_ids: z
38
+ .string()
39
+ .max(200)
40
+ .optional()
41
+ .describe("Comma-separated domain IDs to filter results. Examples: 'Condition', 'Drug'."),
42
+ }, async ({ concept_id, concept_name, query, algorithm, similarity_threshold, page_size, vocabulary_ids, domain_ids, }) => {
43
+ try {
44
+ // Validate exactly one input source
45
+ const provided = [concept_id, concept_name, query].filter((v) => v !== undefined).length;
46
+ if (provided !== 1) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: 'Error: Provide exactly one of concept_id, concept_name, or query.',
52
+ },
53
+ ],
54
+ isError: true,
55
+ };
56
+ }
57
+ const body = {
58
+ algorithm: algorithm ?? 'hybrid',
59
+ similarity_threshold: similarity_threshold ?? 0.7,
60
+ page_size: page_size ?? 20,
61
+ };
62
+ if (concept_id !== undefined)
63
+ body.concept_id = concept_id;
64
+ if (concept_name !== undefined)
65
+ body.concept_name = concept_name;
66
+ if (query !== undefined)
67
+ body.query = query;
68
+ if (vocabulary_ids)
69
+ body.vocabulary_ids = vocabulary_ids
70
+ .split(',')
71
+ .map((s) => s.trim())
72
+ .filter(Boolean);
73
+ if (domain_ids)
74
+ body.domain_ids = domain_ids
75
+ .split(',')
76
+ .map((s) => s.trim())
77
+ .filter(Boolean);
78
+ const response = await client.post('/search/similar', body, 'find_similar_concepts');
79
+ const data = response.data;
80
+ const concepts = data?.similar_concepts ?? [];
81
+ const meta = data?.search_metadata;
82
+ if (concepts.length === 0) {
83
+ const source = concept_id ? `concept ${concept_id}` : (concept_name ?? query ?? '');
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text: `No similar concepts found for ${source}. Try lowering the similarity threshold or using a different algorithm.`,
89
+ },
90
+ {
91
+ type: 'text',
92
+ text: JSON.stringify({ similar_concepts: [], search_metadata: meta }),
93
+ },
94
+ ],
95
+ };
96
+ }
97
+ const source = concept_id
98
+ ? `concept ID ${concept_id}`
99
+ : concept_name
100
+ ? `"${concept_name}"`
101
+ : `"${query}"`;
102
+ const lines = concepts.map((c, i) => {
103
+ const std = c.standard_concept === 'S'
104
+ ? ' [Standard]'
105
+ : c.standard_concept === 'C'
106
+ ? ' [Classification]'
107
+ : '';
108
+ const score = Number.isFinite(c.similarity_score) ? c.similarity_score.toFixed(2) : 'N/A';
109
+ let line = `${i + 1}. **${c.concept_name}** (ID: ${c.concept_id}) — score: ${score}\n ${c.vocabulary_id} | ${c.domain_id} | Code: ${c.concept_code}${std}`;
110
+ if (c.similarity_explanation) {
111
+ line += `\n _${c.similarity_explanation}_`;
112
+ }
113
+ return line;
114
+ });
115
+ const algoLabel = meta?.algorithm_used ?? algorithm ?? 'hybrid';
116
+ const text = `Found ${concepts.length} concepts similar to ${source} (${algoLabel} algorithm):\n\n${lines.join('\n\n')}`;
117
+ return {
118
+ content: [
119
+ { type: 'text', text },
120
+ {
121
+ type: 'text',
122
+ text: JSON.stringify({ similar_concepts: concepts, search_metadata: meta }),
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ catch (error) {
128
+ const { text, json } = formatErrorForMcp(error, 'find_similar_concepts');
129
+ return {
130
+ content: [
131
+ { type: 'text', text },
132
+ { type: 'text', text: json },
133
+ ],
134
+ isError: true,
135
+ };
136
+ }
137
+ });
138
+ }
139
+ //# sourceMappingURL=similar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similar.js","sourceRoot":"","sources":["../../src/tools/similar.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ export function resolveApiKey(cliKey) {
2
2
  const key = cliKey || process.env.OMOPHUB_API_KEY;
3
3
  if (!key) {
4
4
  throw new Error('OMOPHub API key required. Set OMOPHUB_API_KEY environment variable or pass --api-key=KEY.\n' +
5
- 'Get your free API key at: https://omophub.com/dashboard/api-keys');
5
+ 'Get your free API key at: https://dashboard.omophub.com/api-keys');
6
6
  }
7
7
  return key;
8
8
  }
@@ -8,8 +8,8 @@ export class OmopHubApiError extends Error {
8
8
  this.name = 'OmopHubApiError';
9
9
  }
10
10
  }
11
- const UPGRADE_URL = 'https://omophub.com/dashboard/billing';
12
- const API_KEYS_URL = 'https://omophub.com/dashboard/api-keys';
11
+ const UPGRADE_URL = 'https://dashboard.omophub.com/billing';
12
+ const API_KEYS_URL = 'https://dashboard.omophub.com/api-keys';
13
13
  export function formatErrorForMcp(error, toolName) {
14
14
  if (error instanceof OmopHubApiError) {
15
15
  switch (error.status) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omophub/omophub-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.2",
4
4
  "mcpName": "io.github.OMOPHub/omophub-mcp",
5
5
  "description": "MCP server for OHDSI OMOP standardized medical vocabularies — search, lookup, map, and navigate concepts via AI agents",
6
6
  "type": "module",