@nucl-parquet/mcp 0.13.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,27 +1,80 @@
1
+ /**
2
+ * nucl-parquet MCP server — wraps local data via DuckDB.
3
+ *
4
+ * SSoT refactor (epic #173, Sub-D #178): the MCP is a thin shell over
5
+ * DuckDB-backed Parquet views, not a re-implementation of parquet I/O.
6
+ * All data is read from the local data directory — no HTTP fetching.
7
+ */
8
+ import duckdb from "duckdb";
9
+ export declare const PKG_VERSION: string;
1
10
  interface Library {
2
11
  name: string;
3
12
  description: string;
4
- source_url: string;
5
- projectiles: string[];
6
- data_type: string;
13
+ source_url?: string;
14
+ projectiles?: string[];
15
+ data_type?: string;
7
16
  version: string;
17
+ path?: string;
18
+ }
19
+ interface ViewDef {
8
20
  path: string;
21
+ type?: "file" | "glob";
22
+ optional?: boolean;
23
+ note?: string;
9
24
  }
10
25
  interface Catalog {
26
+ /**
27
+ * The data release this server is serving, e.g. `2026.8.3`.
28
+ *
29
+ * Distinct from `Library.version`, which is the *evaluation's* version
30
+ * ("2023-iso"). They answer different questions, and conflating them would
31
+ * make a cross-server check silently wrong rather than merely absent (#348).
32
+ *
33
+ * Read from the catalog on disk, never compiled in — a build-time constant
34
+ * would be a second source of truth for the one fact whose whole job is to
35
+ * identify the data actually being read.
36
+ */
37
+ data_version: string;
11
38
  libraries: Record<string, Library>;
12
- shared: {
13
- meta: {
14
- path: string;
15
- files: Record<string, string>;
16
- };
17
- stopping: {
18
- path: string;
19
- files: Record<string, string>;
20
- sources: string[];
21
- };
22
- };
39
+ views?: Record<string, ViewDef>;
40
+ [key: string]: unknown;
23
41
  }
24
- declare function fetchParquetRows(relativePath: string, baseUrl?: string): Promise<Record<string, unknown>[]>;
25
- declare function getCatalog(): Catalog;
26
-
27
- export { fetchParquetRows, getCatalog };
42
+ export declare function ensureCatalog(): Catalog;
43
+ /**
44
+ * Parse and validate a catalog.
45
+ *
46
+ * Separate from {@link ensureCatalog} because that one memoises a module-level
47
+ * singleton, which a test cannot re-enter — so the validation below would be
48
+ * unreachable from a test and, in practice, unverified.
49
+ */
50
+ export declare function parseCatalog(raw: string, dataDir: string): Catalog;
51
+ /**
52
+ * The `list_libraries` payload: the data release, then the libraries.
53
+ *
54
+ * A named function rather than an inline object so a test can assert the shape
55
+ * the tool actually returns. Asserting only that the catalog *has* a
56
+ * data_version would pass while the tool still returned a bare array.
57
+ */
58
+ export declare function libraryListPayload(cat: Catalog): {
59
+ data_version: string;
60
+ libraries: Array<Record<string, unknown>>;
61
+ };
62
+ export declare function getDb(): duckdb.Database;
63
+ /**
64
+ * Server instructions, naming the data release in the client's context.
65
+ *
66
+ * The referral case #348 is about does not go through `list_libraries`:
67
+ * hyrr-mcp sends an agent straight to `get_cross_sections` for full σ(E)
68
+ * curves, having stated the release *it* computed against. Reporting the
69
+ * release only from a tool the agent has no reason to call would leave that
70
+ * claim as unverifiable as it was — so it is stated here too, where it reaches
71
+ * the model without a round trip, and in `list_libraries` where it can be read
72
+ * programmatically. Both read the same loaded catalog, so they cannot disagree.
73
+ *
74
+ * Kept word-for-word in step with the Rust server's `instructions()`; a test
75
+ * pins that the two do not drift.
76
+ */
77
+ export declare function instructions(dataVersion: string): string;
78
+ export declare const BLOCKED_FUNCTIONS: RegExp;
79
+ export declare const ALLOWED_FIRST_WORDS: Set<string>;
80
+ export {};
package/dist/index.js CHANGED
@@ -1,120 +1,214 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { existsSync, readFileSync, readdirSync } from "fs";
5
+ import { createRequire } from "module";
6
+ import { join, resolve } from "path";
7
+ import { homedir } from "os";
4
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { parquetRead } from "hyparquet";
7
- import { compressors } from "hyparquet-compressors";
10
+ import duckdb from "duckdb";
8
11
  import { z } from "zod";
9
- var BASE_URL = process.env.NUCL_PARQUET_BASE_URL ?? "https://raw.githubusercontent.com/exoma-ch/nucl-parquet/main/data/";
10
- var CATALOG = {
11
- libraries: {
12
- "tendl-2024": { name: "TENDL-2024", description: "TALYS Evaluated Nuclear Data Library 2024 (IAEA/PSI)", source_url: "https://tendl.web.psi.ch/tendl_2024/tendl2024.html", projectiles: ["p", "d", "t", "h", "a"], data_type: "cross_sections", version: "2024", path: "tendl-2024/xs/" },
13
- "endfb-8.1": { name: "ENDF/B-VIII.1", description: "US Evaluated Nuclear Data File (NNDC/BNL)", source_url: "https://www.nndc.bnl.gov/endf-b8.1/", projectiles: ["n", "p", "d", "t", "h", "a"], data_type: "cross_sections", version: "VIII.1", path: "endfb-8.1/xs/" },
14
- "jeff-4.0": { name: "JEFF-4.0", description: "Joint Evaluated Fission and Fusion File (NEA)", source_url: "https://www.oecd-nea.org/dbdata/jeff/", projectiles: ["n", "p"], data_type: "cross_sections", version: "4.0", path: "jeff-4.0/xs/" },
15
- "jendl-5": { name: "JENDL-5", description: "Japanese Evaluated Nuclear Data Library (JAEA)", source_url: "https://wwwndc.jaea.go.jp/jendl/j5/j5.html", projectiles: ["n", "p", "d", "a"], data_type: "cross_sections", version: "5", path: "jendl-5/xs/" },
16
- "tendl-2025": { name: "TENDL-2025", description: "TALYS Evaluated Nuclear Data Library 2025 (PSI)", source_url: "https://tendl.web.psi.ch/", projectiles: ["n", "p", "d", "t", "h", "a"], data_type: "cross_sections", version: "2025", path: "tendl-2025/xs/" },
17
- "cendl-3.2": { name: "CENDL-3.2", description: "Chinese Evaluated Nuclear Data Library (CIAE)", source_url: "http://www.nuclear.csdb.cn/", projectiles: ["n"], data_type: "cross_sections", version: "3.2", path: "cendl-3.2/xs/" },
18
- "brond-3.1": { name: "BROND-3.1", description: "Russian Evaluated Nuclear Data Library (IPPE)", source_url: "https://vant.ippe.ru/", projectiles: ["n"], data_type: "cross_sections", version: "3.1", path: "brond-3.1/xs/" },
19
- "fendl-3.2": { name: "FENDL-3.2", description: "Fusion Evaluated Nuclear Data Library (IAEA)", source_url: "https://www-nds.iaea.org/fendl/", projectiles: ["n"], data_type: "cross_sections", version: "3.2", path: "fendl-3.2/xs/" },
20
- "eaf-2010": { name: "EAF-2010", description: "European Activation File (CCFE)", source_url: "https://fispact.ukaea.uk/", projectiles: ["n"], data_type: "cross_sections", version: "2010", path: "eaf-2010/xs/" },
21
- "irdff-2": { name: "IRDFF-II", description: "International Reactor Dosimetry and Fusion File (IAEA)", source_url: "https://www-nds.iaea.org/IRDFF/", projectiles: ["n"], data_type: "cross_sections", version: "II", path: "irdff-2/xs/" },
22
- "iaea-medical": { name: "IAEA-Medical", description: "Medical isotope production cross-sections (IAEA)", source_url: "https://www-nds.iaea.org/medical/", projectiles: ["p", "d", "h", "a"], data_type: "cross_sections", version: "latest", path: "iaea-medical/xs/" },
23
- "jendl-ad-2017": { name: "JENDL/AD-2017", description: "Activation/Dosimetry Library (JAEA)", source_url: "https://wwwndc.jaea.go.jp/jendl/jad/jad.html", projectiles: ["n", "p"], data_type: "cross_sections", version: "2017", path: "jendl-ad-2017/xs/" },
24
- "jendl-deu-2020": { name: "JENDL-DEU-2020", description: "Dedicated deuteron-induced reaction library (JAEA)", source_url: "https://wwwndc.jaea.go.jp/jendl/deu/deu.html", projectiles: ["d"], data_type: "cross_sections", version: "2020", path: "jendl-deu-2020/xs/" },
25
- "iaea-pd-2019": { name: "IAEA-PD-2019", description: "Photonuclear Data Library (IAEA)", source_url: "https://www-nds.iaea.org/photonuclear/", projectiles: ["g"], data_type: "cross_sections", version: "2019", path: "iaea-pd-2019/xs/" },
26
- "exfor": { name: "EXFOR", description: "Experimental nuclear reaction data (IAEA NDS)", source_url: "https://www-nds.iaea.org/exfor/", projectiles: ["n", "p", "d", "t", "h", "a"], data_type: "experimental_cross_sections", version: "latest", path: "exfor/" }
27
- },
28
- shared: {
29
- meta: { path: "meta/", files: { abundances: "abundances.parquet", decay: "decay.parquet", elements: "elements.parquet" } },
30
- stopping: {
31
- path: "stopping/",
32
- files: {
33
- PSTAR: "PSTAR.parquet",
34
- ASTAR: "ASTAR.parquet",
35
- ESTAR: "ESTAR.parquet",
36
- dSTAR: "dSTAR.parquet",
37
- tSTAR: "tSTAR.parquet",
38
- catima: "catima/catima.parquet"
39
- },
40
- sources: ["PSTAR", "ASTAR", "ESTAR", "dSTAR", "tSTAR", "catima"]
12
+ var require2 = createRequire(import.meta.url);
13
+ var { version: PKG_VERSION } = require2("../package.json");
14
+ function resolveDataDir() {
15
+ const envDir = process.env.NUCL_PARQUET_DATA;
16
+ if (envDir && existsSync(envDir)) return resolve(envDir);
17
+ const repoDir = resolve(import.meta.dirname ?? ".", "..", "..", "..", "..", "data");
18
+ if (existsSync(join(repoDir, "catalog.json"))) return repoDir;
19
+ const homeData = join(homedir(), ".nucl-parquet");
20
+ if (existsSync(join(homeData, "catalog.json"))) return homeData;
21
+ throw new Error(
22
+ "Cannot find nucl-parquet data directory. Set NUCL_PARQUET_DATA env var or place data at ~/.nucl-parquet/. Data releases: https://github.com/exoma-ch/nucl-parquet/releases"
23
+ );
24
+ }
25
+ var _catalog;
26
+ function ensureCatalog() {
27
+ if (_catalog) return _catalog;
28
+ const dataDir = resolveDataDir();
29
+ _catalog = parseCatalog(readFileSync(join(dataDir, "catalog.json"), "utf-8"), dataDir);
30
+ return _catalog;
31
+ }
32
+ function parseCatalog(raw, dataDir) {
33
+ const parsed = JSON.parse(raw);
34
+ if (typeof parsed.data_version !== "string" || parsed.data_version === "") {
35
+ throw new Error(
36
+ `catalog.json at ${dataDir} has no 'data_version'. It is required by data/catalog.schema.json and identifies the data release this server serves; refusing to start rather than report an unknown release as if it were known.`
37
+ );
38
+ }
39
+ return parsed;
40
+ }
41
+ function libraryListPayload(cat) {
42
+ return {
43
+ // Beside the array rather than on each entry, so the release and a
44
+ // library's evaluation `version` cannot be read as the same kind of thing.
45
+ data_version: cat.data_version,
46
+ libraries: Object.entries(cat.libraries).filter(([, lib]) => lib.projectiles).map(([id, lib]) => ({
47
+ id,
48
+ name: lib.name,
49
+ description: lib.description,
50
+ projectiles: lib.projectiles,
51
+ version: lib.version,
52
+ data_type: lib.data_type
53
+ }))
54
+ };
55
+ }
56
+ var _db;
57
+ function hasParquetFiles(dir) {
58
+ if (!existsSync(dir)) return false;
59
+ try {
60
+ return readdirSync(dir).some((f) => f.endsWith(".parquet"));
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ function registerParquet(db, filePath, viewName) {
66
+ if (existsSync(filePath)) {
67
+ db.run(`CREATE VIEW ${viewName} AS SELECT * FROM read_parquet('${filePath}')`);
68
+ }
69
+ }
70
+ function registerGlob(db, dir, viewName) {
71
+ if (hasParquetFiles(dir)) {
72
+ const glob = join(dir, "*.parquet");
73
+ db.run(`CREATE VIEW ${viewName} AS SELECT * FROM read_parquet('${glob}')`);
74
+ }
75
+ }
76
+ function registerViews(db, dataDir) {
77
+ const catalog = ensureCatalog();
78
+ const libViews = [];
79
+ for (const [libKey, lib] of Object.entries(catalog.libraries)) {
80
+ if (!lib.path) continue;
81
+ const libDir = join(dataDir, lib.path);
82
+ if (!hasParquetFiles(libDir)) continue;
83
+ const viewName = libKey.replace(/-/g, "_").replace(/\./g, "_");
84
+ const glob = join(libDir, "*.parquet");
85
+ db.run(
86
+ `CREATE VIEW ${viewName} AS SELECT *, '${libKey}' AS library FROM read_parquet('${glob}', filename=true)`
87
+ );
88
+ if (lib.data_type === "cross_sections") libViews.push(viewName);
89
+ }
90
+ if (libViews.length > 0) {
91
+ const union = libViews.map((v) => `SELECT * FROM ${v}`).join(" UNION ALL ");
92
+ db.run(`CREATE VIEW xs AS ${union}`);
93
+ }
94
+ for (const [viewName, viewDef] of Object.entries(catalog.views ?? {})) {
95
+ const viewPath = join(dataDir, viewDef.path);
96
+ if (viewDef.type === "glob") {
97
+ registerGlob(db, viewPath, viewName);
98
+ } else {
99
+ registerParquet(db, viewPath, viewName);
41
100
  }
42
101
  }
43
- };
44
- var parquetCache = /* @__PURE__ */ new Map();
45
- async function fetchParquetRows(relativePath, baseUrl = BASE_URL) {
46
- if (parquetCache.has(relativePath)) return parquetCache.get(relativePath);
47
- const url = new URL(relativePath, baseUrl).href;
48
- const resp = await fetch(url);
49
- if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching ${url}`);
50
- const buffer = await resp.arrayBuffer();
51
- const rows = [];
52
- await parquetRead({
53
- file: buffer,
54
- compressors,
55
- rowFormat: "object",
56
- onComplete: (data) => {
57
- rows.push(...data);
102
+ const nuclidesPath = join(dataDir, "meta", "ensdf", "nuclides.parquet");
103
+ if (existsSync(nuclidesPath)) {
104
+ db.run("CREATE OR REPLACE VIEW ground_states AS SELECT * FROM nuclides WHERE state = 'g'");
105
+ }
106
+ const eadlDir = join(dataDir, "meta", "eadl");
107
+ if (hasParquetFiles(eadlDir)) {
108
+ db.run("CREATE VIEW eadl_transitions AS SELECT * FROM atomic_relaxation");
109
+ db.run("CREATE VIEW fluorescence AS SELECT * FROM atomic_relaxation WHERE transition_type = 'radiative'");
110
+ }
111
+ }
112
+ function harden(db, dataDir) {
113
+ const quoted = dataDir.replaceAll("'", "''");
114
+ db.run(`SET allowed_directories = ['${quoted}']`);
115
+ db.run("SET enable_external_access = false");
116
+ db.run("SET autoinstall_known_extensions = false");
117
+ db.run("SET autoload_known_extensions = false");
118
+ db.run("SET lock_configuration = true");
119
+ }
120
+ function getDb() {
121
+ if (_db) return _db;
122
+ const dataDir = resolveDataDir();
123
+ const db = new duckdb.Database(":memory:");
124
+ registerViews(db, dataDir);
125
+ harden(db, dataDir);
126
+ _db = db;
127
+ return db;
128
+ }
129
+ function dbAll(db, sql, params) {
130
+ return new Promise((resolve2, reject) => {
131
+ const cb = (err, rows) => {
132
+ if (err) reject(err);
133
+ else resolve2(rows);
134
+ };
135
+ if (params && params.length > 0) {
136
+ db.all(sql, ...params, cb);
137
+ } else {
138
+ db.all(sql, cb);
58
139
  }
59
140
  });
60
- parquetCache.set(relativePath, rows);
61
- return rows;
62
141
  }
63
- async function fetchManifest(libraryId) {
64
- const lib = CATALOG.libraries[libraryId];
65
- if (!lib) throw new Error(`Unknown library: ${libraryId}`);
66
- const manifestPath = lib.path.replace(/xs\/$/, "manifest.json");
67
- const url = new URL(manifestPath, BASE_URL).href;
68
- const resp = await fetch(url);
69
- if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching manifest for ${libraryId}`);
70
- return await resp.json();
142
+ function safeStringify(obj, indent) {
143
+ return JSON.stringify(
144
+ obj,
145
+ (_key, value) => typeof value === "bigint" ? Number(value) : value,
146
+ indent
147
+ );
71
148
  }
72
- function getCatalog() {
73
- return CATALOG;
149
+ async function query(sql, params, maxRows = 500) {
150
+ const db = getDb();
151
+ const countSql = `SELECT CAST(COUNT(*) AS INTEGER) AS n FROM (${sql})`;
152
+ const countRows = await dbAll(db, countSql, params);
153
+ const total = countRows[0]?.n ?? 0;
154
+ const limitSql = `${sql} LIMIT ${maxRows}`;
155
+ const rows = await dbAll(db, limitSql, params);
156
+ return { total, truncated: total > maxRows, rows };
74
157
  }
75
- function truncateRows(rows, limit) {
76
- if (rows.length <= limit) return { rows, truncated: false, total: rows.length };
77
- return { rows: rows.slice(0, limit), truncated: true, total: rows.length };
158
+ function instructions(dataVersion) {
159
+ return `Serving nucl-parquet data release ${dataVersion}. This identifies the *data*, not this server's version \u2014 report it whenever you compare results with another server or tool. If another source states a different release, say so: agreement or disagreement computed across two releases is an artefact of the mismatch, not a physics result. Call list_libraries to read the same value programmatically.`;
78
160
  }
79
- var server = new McpServer({
80
- name: "nucl-parquet",
81
- version: "0.3.7"
82
- });
161
+ var server = new McpServer(
162
+ {
163
+ name: "nucl-parquet",
164
+ // The package version — the software, not the data. Both are reported
165
+ // because they answer different questions, and only one of them changes
166
+ // when the data does.
167
+ version: PKG_VERSION
168
+ },
169
+ { instructions: instructions(ensureCatalog().data_version) }
170
+ );
83
171
  server.tool(
84
172
  "list_libraries",
85
173
  "List all available nuclear data libraries with projectiles and descriptions",
86
174
  {},
87
175
  async () => {
88
- const libs = Object.entries(CATALOG.libraries).map(([id, lib]) => ({
89
- id,
90
- name: lib.name,
91
- description: lib.description,
92
- projectiles: lib.projectiles,
93
- version: lib.version,
94
- data_type: lib.data_type
95
- }));
96
- return { content: [{ type: "text", text: JSON.stringify(libs, null, 2) }] };
176
+ return {
177
+ content: [
178
+ { type: "text", text: safeStringify(libraryListPayload(ensureCatalog()), 2) }
179
+ ]
180
+ };
97
181
  }
98
182
  );
99
183
  server.tool(
100
184
  "list_isotopes",
101
185
  "List available target elements for a given library and projectile. Returns element symbols.",
102
186
  {
103
- library: z.string().describe("Library ID, e.g. 'tendl-2024', 'endfb-8.1'"),
187
+ library: z.string().describe("Library ID, e.g. 'tendl-2025', 'endfb-8.1'"),
104
188
  projectile: z.string().describe("Projectile: n, p, d, t, h, a, g")
105
189
  },
106
190
  async ({ library, projectile }) => {
107
- const lib = CATALOG.libraries[library];
191
+ const cat = ensureCatalog();
192
+ const lib = cat.libraries[library];
108
193
  if (!lib) throw new Error(`Unknown library: ${library}. Use list_libraries to see available libraries.`);
109
- if (!lib.projectiles.includes(projectile)) {
110
- throw new Error(`Projectile '${projectile}' not available for ${library}. Available: ${lib.projectiles.join(", ")}`);
194
+ if (!lib.projectiles?.includes(projectile)) {
195
+ throw new Error(`Projectile '${projectile}' not available for ${library}. Available: ${lib.projectiles?.join(", ") ?? "none"}`);
196
+ }
197
+ const dataDir = resolveDataDir();
198
+ const libPath = lib.path;
199
+ if (!libPath?.endsWith("xs/")) {
200
+ throw new Error(`Library ${library} path does not end with 'xs/' \u2014 cannot derive manifest path`);
201
+ }
202
+ const manifestPath = join(dataDir, libPath.replace("xs/", "manifest.json"));
203
+ if (!existsSync(manifestPath)) {
204
+ throw new Error(`Manifest not found at ${manifestPath}`);
111
205
  }
112
- const manifest = await fetchManifest(library);
206
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
113
207
  const elements = manifest.elements ?? [];
114
208
  return {
115
209
  content: [{
116
210
  type: "text",
117
- text: JSON.stringify({ library, projectile, elements, count: elements.length }, null, 2)
211
+ text: safeStringify({ library, projectile, elements, count: elements.length }, 2)
118
212
  }]
119
213
  };
120
214
  }
@@ -123,26 +217,36 @@ server.tool(
123
217
  "get_cross_sections",
124
218
  "Get nuclear reaction cross-section data for a specific target element. Returns energy (MeV) and cross-section (mb) arrays with reaction product info.",
125
219
  {
126
- library: z.string().describe("Library ID, e.g. 'tendl-2024'"),
220
+ library: z.string().describe("Library ID, e.g. 'tendl-2025'"),
127
221
  projectile: z.string().describe("Projectile: n, p, d, t, h, a, g"),
128
222
  element: z.string().describe("Target element symbol, e.g. 'Cu', 'Fe', 'Au'"),
129
223
  max_rows: z.number().optional().describe("Max rows to return (default 500)")
130
224
  },
131
225
  async ({ library, projectile, element, max_rows }) => {
132
- const lib = CATALOG.libraries[library];
226
+ if (!/^[npdthag]$/.test(projectile)) {
227
+ throw new Error(`Invalid projectile: '${projectile}'. Must be one of: n, p, d, t, h, a, g`);
228
+ }
229
+ if (!/^[A-Z][a-z]?$/.test(element)) {
230
+ throw new Error(`Invalid element symbol: '${element}'. Must be 1-2 letters (e.g. 'Cu', 'Fe')`);
231
+ }
232
+ const cat = ensureCatalog();
233
+ const lib = cat.libraries[library];
133
234
  if (!lib) throw new Error(`Unknown library: ${library}`);
134
- const parquetPath = `${lib.path}${projectile}_${element}.parquet`;
135
- const rows = await fetchParquetRows(parquetPath);
136
- const result = truncateRows(rows, max_rows ?? 500);
235
+ const dataDir = resolveDataDir();
236
+ const parquetPath = join(dataDir, `${lib.path}${projectile}_${element}.parquet`);
237
+ if (!existsSync(parquetPath)) {
238
+ throw new Error(`No data for ${projectile}_${element} in ${library}`);
239
+ }
240
+ const db = getDb();
241
+ const result = await query(
242
+ `SELECT * FROM read_parquet('${parquetPath}')`,
243
+ [],
244
+ max_rows ?? 500
245
+ );
137
246
  return {
138
247
  content: [{
139
248
  type: "text",
140
- text: JSON.stringify({
141
- library,
142
- projectile,
143
- element,
144
- ...result
145
- }, null, 2)
249
+ text: safeStringify({ library, projectile, element, ...result }, 2)
146
250
  }]
147
251
  };
148
252
  }
@@ -158,17 +262,24 @@ server.tool(
158
262
  if (zNum === void 0 && aNum === void 0) {
159
263
  throw new Error("Provide at least z or a to filter decay data.");
160
264
  }
161
- const metaPath = CATALOG.shared.meta.path + CATALOG.shared.meta.files.decay;
162
- const rows = await fetchParquetRows(metaPath);
163
- const filtered = rows.filter((row) => {
164
- if (zNum !== void 0 && row["Z"] !== zNum) return false;
165
- if (aNum !== void 0 && row["A"] !== aNum) return false;
166
- return true;
167
- });
265
+ const conditions = [];
266
+ const params = [];
267
+ if (zNum !== void 0) {
268
+ conditions.push("Z = ?");
269
+ params.push(zNum);
270
+ }
271
+ if (aNum !== void 0) {
272
+ conditions.push("A = ?");
273
+ params.push(aNum);
274
+ }
275
+ const result = await query(
276
+ `SELECT * FROM decay WHERE ${conditions.join(" AND ")}`,
277
+ params
278
+ );
168
279
  return {
169
280
  content: [{
170
281
  type: "text",
171
- text: JSON.stringify({ z: zNum, a: aNum, count: filtered.length, rows: filtered }, null, 2)
282
+ text: safeStringify({ z: zNum, a: aNum, count: result.total, rows: result.rows }, 2)
172
283
  }]
173
284
  };
174
285
  }
@@ -180,13 +291,11 @@ server.tool(
180
291
  z: z.number().describe("Atomic number (e.g. 29 for Cu)")
181
292
  },
182
293
  async ({ z: zNum }) => {
183
- const metaPath = CATALOG.shared.meta.path + CATALOG.shared.meta.files.abundances;
184
- const rows = await fetchParquetRows(metaPath);
185
- const filtered = rows.filter((row) => row["Z"] === zNum);
294
+ const result = await query("SELECT * FROM abundances WHERE Z = ?", [zNum]);
186
295
  return {
187
296
  content: [{
188
297
  type: "text",
189
- text: JSON.stringify({ z: zNum, count: filtered.length, isotopes: filtered }, null, 2)
298
+ text: safeStringify({ z: zNum, count: result.total, isotopes: result.rows }, 2)
190
299
  }]
191
300
  };
192
301
  }
@@ -196,34 +305,336 @@ server.tool(
196
305
  "Get mass stopping power (dE/dx) data for a projectile in a target element.",
197
306
  {
198
307
  source: z.string().describe(
199
- "Data source: PSTAR (protons), ASTAR (\u03B1, NIST ICRU-49), ESTAR (electrons), dSTAR, tSTAR (velocity-scaled from PSTAR), or catima (full Z\xD7Z table; \u03B1/\xB3He fallback for non-NIST elements)"
308
+ "Data source: PSTAR (protons), ASTAR (\u03B1, NIST ICRU-49), ESTAR (electrons), dSTAR, tSTAR (velocity-scaled from PSTAR), or catima (full Z\xD7Z table)"
200
309
  ),
201
310
  target_z: z.number().describe("Target element atomic number")
202
311
  },
203
312
  async ({ source, target_z }) => {
204
- const fileMap = CATALOG.shared.stopping.files;
205
- if (!(source in fileMap)) {
313
+ const viewMap = {
314
+ PSTAR: "stopping",
315
+ ASTAR: "stopping",
316
+ ESTAR: "stopping",
317
+ dSTAR: "stopping",
318
+ tSTAR: "stopping",
319
+ catima: "catima_stopping"
320
+ };
321
+ if (!(source in viewMap)) {
322
+ throw new Error(`Unknown source '${source}'. Valid: PSTAR, ASTAR, ESTAR, dSTAR, tSTAR, catima`);
323
+ }
324
+ const view = viewMap[source];
325
+ const sql = source === "catima" ? `SELECT * FROM ${view} WHERE target_Z = ?` : `SELECT * FROM ${view} WHERE source = ? AND target_Z = ?`;
326
+ const params = source === "catima" ? [target_z] : [source, target_z];
327
+ const result = await query(sql, params);
328
+ return {
329
+ content: [{
330
+ type: "text",
331
+ text: safeStringify({ source, target_z, count: result.total, rows: result.rows }, 2)
332
+ }]
333
+ };
334
+ }
335
+ );
336
+ server.tool(
337
+ "get_radiation",
338
+ "Get radiation emissions (gammas, X-rays, Auger electrons, conversion electrons) for a nuclide.",
339
+ {
340
+ z: z.number().describe("Atomic number of the parent nuclide"),
341
+ a: z.number().optional().describe("Mass number (omit for all isotopes of element Z)"),
342
+ max_rows: z.number().optional().describe("Max rows to return (default 500)")
343
+ },
344
+ async ({ z: zNum, a: aNum, max_rows }) => {
345
+ const conditions = ["Z = ?"];
346
+ const params = [zNum];
347
+ if (aNum !== void 0) {
348
+ conditions.push("A = ?");
349
+ params.push(aNum);
350
+ }
351
+ const result = await query(
352
+ `SELECT * FROM radiation WHERE ${conditions.join(" AND ")}`,
353
+ params,
354
+ max_rows ?? 500
355
+ );
356
+ return {
357
+ content: [{
358
+ type: "text",
359
+ text: safeStringify({ z: zNum, a: aNum, ...result }, 2)
360
+ }]
361
+ };
362
+ }
363
+ );
364
+ server.tool(
365
+ "get_coincidences",
366
+ "Get gamma-gamma and mixed-emission coincidence pairs for a nuclide. Includes beta/EC/X-ray/Auger/511 keV annihilation paired with gammas.",
367
+ {
368
+ z: z.number().describe("Atomic number of the parent nuclide"),
369
+ a: z.number().optional().describe("Mass number (omit for all isotopes of element Z)"),
370
+ max_rows: z.number().optional().describe("Max rows to return (default 500)")
371
+ },
372
+ async ({ z: zNum, a: aNum, max_rows }) => {
373
+ const conditions = ["Z = ?"];
374
+ const params = [zNum];
375
+ if (aNum !== void 0) {
376
+ conditions.push("A = ?");
377
+ params.push(aNum);
378
+ }
379
+ const result = await query(
380
+ `SELECT * FROM coincidences WHERE ${conditions.join(" AND ")}`,
381
+ params,
382
+ max_rows ?? 500
383
+ );
384
+ return {
385
+ content: [{
386
+ type: "text",
387
+ text: safeStringify({ z: zNum, a: aNum, ...result }, 2)
388
+ }]
389
+ };
390
+ }
391
+ );
392
+ server.tool(
393
+ "get_summing_partners",
394
+ "Get ICC-corrected summing partners for HPGe true-coincidence-summing (TCS) corrections. Returns emission pairs with pre-computed icc_correction_factor and pure_emission_joint_intensity.",
395
+ {
396
+ z: z.number().describe("Atomic number of the daughter nuclide (filing convention)"),
397
+ a: z.number().describe("Mass number"),
398
+ primary_energy_keV: z.number().optional().describe("Filter to pairs matching this energy on either side"),
399
+ tolerance_keV: z.number().optional().describe("Energy match tolerance in keV (default 0.5)"),
400
+ emission1_rad_type: z.string().optional().describe("Filter side 1: 'gamma', 'xray', 'auger'"),
401
+ max_rows: z.number().optional().describe("Max rows to return (default 500)")
402
+ },
403
+ async ({ z: zNum, a: aNum, primary_energy_keV, tolerance_keV, emission1_rad_type, max_rows }) => {
404
+ const conditions = ["Z = ?", "A = ?"];
405
+ const params = [zNum, aNum];
406
+ const tol = tolerance_keV ?? 0.5;
407
+ if (primary_energy_keV !== void 0) {
408
+ conditions.push("(ABS(emission1_energy_keV - ?) < ? OR ABS(emission2_energy_keV - ?) < ?)");
409
+ params.push(primary_energy_keV, tol, primary_energy_keV, tol);
410
+ }
411
+ if (emission1_rad_type !== void 0) {
412
+ conditions.push("emission1_rad_type = ?");
413
+ params.push(emission1_rad_type);
414
+ }
415
+ const result = await query(
416
+ `SELECT * FROM summing_partners WHERE ${conditions.join(" AND ")} ORDER BY pure_emission_joint_intensity DESC`,
417
+ params,
418
+ max_rows ?? 500
419
+ );
420
+ return {
421
+ content: [{
422
+ type: "text",
423
+ text: safeStringify({ z: zNum, a: aNum, primary_energy_keV, ...result }, 2)
424
+ }]
425
+ };
426
+ }
427
+ );
428
+ server.tool(
429
+ "get_emissions",
430
+ "Get absolute per-decay photon emission intensities (NuDat-equivalent). Returns gamma emissions for a parent nuclide with absolute intensities (photon emission probability per decay, 0-100%). Filed by parent, not daughter.",
431
+ {
432
+ parent_z: z.number().describe("Atomic number of the decaying parent (e.g. 27 for Co-60)"),
433
+ parent_a: z.number().describe("Mass number of the parent (e.g. 60 for Co-60)"),
434
+ parent_state: z.string().optional().describe("Nuclear state: '' (ground), 'm', 'm2'"),
435
+ decay_mode: z.string().optional().describe("Filter by decay mode: 'beta-', 'KshellEC', 'IT', etc."),
436
+ energy_keV: z.number().optional().describe("Filter to gammas near this energy"),
437
+ tolerance_keV: z.number().optional().describe("Energy tolerance (default 0.5 keV)"),
438
+ min_intensity_pct: z.number().optional().describe("Minimum absolute intensity (%) to include"),
439
+ max_rows: z.number().optional().describe("Max rows to return (default 500)")
440
+ },
441
+ async ({ parent_z, parent_a, parent_state, decay_mode, energy_keV, tolerance_keV, min_intensity_pct, max_rows }) => {
442
+ const conditions = ["parent_Z = ?", "parent_A = ?", "parent_state = ?"];
443
+ const params = [parent_z, parent_a, parent_state ?? ""];
444
+ const tol = tolerance_keV ?? 0.5;
445
+ if (decay_mode !== void 0) {
446
+ conditions.push("decay_mode = ?");
447
+ params.push(decay_mode);
448
+ }
449
+ if (energy_keV !== void 0) {
450
+ conditions.push("ABS(energy_keV - ?) < ?");
451
+ params.push(energy_keV, tol);
452
+ }
453
+ if (min_intensity_pct !== void 0 && min_intensity_pct > 0) {
454
+ conditions.push("intensity_pct >= ?");
455
+ params.push(min_intensity_pct);
456
+ }
457
+ const result = await query(
458
+ `SELECT * FROM emissions WHERE ${conditions.join(" AND ")} ORDER BY intensity_pct DESC`,
459
+ params,
460
+ max_rows ?? 500
461
+ );
462
+ return {
463
+ content: [{
464
+ type: "text",
465
+ text: safeStringify({ parent_z, parent_a, parent_state: parent_state ?? "", ...result }, 2)
466
+ }]
467
+ };
468
+ }
469
+ );
470
+ server.tool(
471
+ "get_beta_spectrum",
472
+ "Get the continuous beta-decay kinetic-energy spectrum for a nuclide. Returns pre-tabulated Fermi-function spectra (dN/dE normalized to 1).",
473
+ {
474
+ z: z.number().describe("Atomic number of the parent nuclide"),
475
+ a: z.number().describe("Mass number of the parent nuclide"),
476
+ max_rows: z.number().optional().describe("Max rows to return (default 500)")
477
+ },
478
+ async ({ z: zNum, a: aNum, max_rows }) => {
479
+ const result = await query(
480
+ "SELECT * FROM beta_spectra WHERE Z = ? AND A = ?",
481
+ [zNum, aNum],
482
+ max_rows ?? 500
483
+ );
484
+ return {
485
+ content: [{
486
+ type: "text",
487
+ text: safeStringify({ z: zNum, a: aNum, ...result }, 2)
488
+ }]
489
+ };
490
+ }
491
+ );
492
+ server.tool(
493
+ "get_compound_compositions",
494
+ "Get elemental compositions (weight fractions) for NIST XCOM standard materials. Useful for Bragg-additive cross-section calculations.",
495
+ {
496
+ material: z.string().optional().describe("Material key (e.g. 'water', 'air', 'concrete'). Omit to list all materials.")
497
+ },
498
+ async ({ material }) => {
499
+ const db = getDb();
500
+ if (material === void 0) {
501
+ const rows = await dbAll(db, "SELECT DISTINCT material FROM compound_compositions ORDER BY material");
502
+ const materials = rows.map((r) => r.material);
206
503
  return {
207
504
  content: [{
208
505
  type: "text",
209
- text: JSON.stringify({
210
- error: `unknown source ${source}; valid: ${CATALOG.shared.stopping.sources.join(", ")}`
211
- }, null, 2)
506
+ text: safeStringify({ count: materials.length, materials }, 2)
212
507
  }]
213
508
  };
214
509
  }
215
- const stoppingPath = CATALOG.shared.stopping.path + fileMap[source];
216
- const rows = await fetchParquetRows(stoppingPath);
217
- const filtered = rows.filter((row) => row["target_Z"] === target_z);
510
+ const result = await query(
511
+ "SELECT * FROM compound_compositions WHERE material = ?",
512
+ [material]
513
+ );
514
+ if (result.total === 0) {
515
+ const allRows = await dbAll(db, "SELECT DISTINCT material FROM compound_compositions ORDER BY material");
516
+ const all = allRows.map((r) => r.material);
517
+ throw new Error(`Unknown material: '${material}'. Available: ${all.join(", ")}`);
518
+ }
519
+ return {
520
+ content: [{
521
+ type: "text",
522
+ text: safeStringify({ material, count: result.total, composition: result.rows }, 2)
523
+ }]
524
+ };
525
+ }
526
+ );
527
+ server.tool(
528
+ "get_electron_stopping",
529
+ "Get electron stopping power with collision/radiative split. Richer than ESTAR \u2014 includes ~183 compounds plus all elements Z=1..98.",
530
+ {
531
+ target: z.string().optional().describe("Compound name (e.g. 'G4_WATER'). For elements use target_z instead."),
532
+ target_z: z.number().optional().describe("Atomic number for elemental targets"),
533
+ max_rows: z.number().optional().describe("Max rows to return (default 500)")
534
+ },
535
+ async ({ target, target_z, max_rows }) => {
536
+ if (target === void 0 && target_z === void 0) {
537
+ throw new Error("Provide target (compound name) or target_z (atomic number).");
538
+ }
539
+ let result;
540
+ if (target_z !== void 0) {
541
+ result = await query(
542
+ "SELECT * FROM electron_stopping WHERE target_Z = ?",
543
+ [target_z],
544
+ max_rows ?? 500
545
+ );
546
+ } else {
547
+ result = await query(
548
+ "SELECT * FROM electron_stopping WHERE name = ? OR g4_name = ?",
549
+ [target, target],
550
+ max_rows ?? 500
551
+ );
552
+ }
553
+ return {
554
+ content: [{
555
+ type: "text",
556
+ text: safeStringify({ target, target_z, ...result }, 2)
557
+ }]
558
+ };
559
+ }
560
+ );
561
+ var BLOCKED_FUNCTIONS = /\b(read_parquet|parquet_scan|parquet_metadata|parquet_schema|read_csv|read_csv_auto|read_json|read_json_auto|read_text|read_blob|glob|copy|export|attach|load|install|create|drop|alter|insert|update|delete|truncate|query_table|pragma)\b/i;
562
+ var ALLOWED_FIRST_WORDS = /* @__PURE__ */ new Set(["SELECT", "WITH", "EXPLAIN", "DESCRIBE", "SHOW", "SUMMARIZE"]);
563
+ server.tool(
564
+ "sql_query",
565
+ "Execute read-only SQL against all 70+ nuclear data tables. Supports JOINs, aggregations, window functions. Use describe_schema() to discover available tables and columns.",
566
+ {
567
+ sql: z.string().describe("Read-only SQL query. DDL/DML will be rejected."),
568
+ max_rows: z.number().optional().describe("Max rows to return (default 10000)")
569
+ },
570
+ async ({ sql: userSql, max_rows }) => {
571
+ const stripped = userSql.trim();
572
+ if (!stripped) throw new Error("Empty SQL query");
573
+ const firstWord = stripped.split(/\s/)[0].toUpperCase();
574
+ if (!ALLOWED_FIRST_WORDS.has(firstWord)) {
575
+ throw new Error(`Only read queries are allowed (SELECT, WITH, EXPLAIN, DESCRIBE). Got: ${firstWord}`);
576
+ }
577
+ if (BLOCKED_FUNCTIONS.test(stripped)) {
578
+ throw new Error("Only read queries are allowed (SELECT, WITH, EXPLAIN, DESCRIBE). File-access and DDL functions are blocked.");
579
+ }
580
+ const db = getDb();
581
+ const limit = max_rows ?? 1e4;
582
+ let total;
583
+ let rows;
584
+ try {
585
+ const countRows = await dbAll(db, `SELECT CAST(COUNT(*) AS INTEGER) AS n FROM (${stripped})`);
586
+ total = countRows[0]?.n ?? 0;
587
+ rows = await dbAll(db, `SELECT * FROM (${stripped}) LIMIT ${limit}`);
588
+ } catch (e) {
589
+ throw new Error(`SQL error: ${e.message}`);
590
+ }
591
+ const truncated = total > limit;
592
+ const display = rows;
593
+ return {
594
+ content: [{
595
+ type: "text",
596
+ text: safeStringify({ total, truncated, rows: display }, 2)
597
+ }]
598
+ };
599
+ }
600
+ );
601
+ server.tool(
602
+ "describe_schema",
603
+ "List all available tables/views with their column names and types. Use this to discover what data is available before writing SQL queries.",
604
+ {},
605
+ async () => {
606
+ const db = getDb();
607
+ const tables = await dbAll(db, "SHOW TABLES");
608
+ const tableNames = tables.map((r) => r.name).sort();
609
+ const schema = {};
610
+ for (const tbl of tableNames) {
611
+ try {
612
+ const cols = await dbAll(db, `DESCRIBE ${tbl}`);
613
+ schema[tbl] = cols.map((c) => ({ name: c.column_name, type: c.column_type }));
614
+ } catch {
615
+ schema[tbl] = [];
616
+ }
617
+ }
618
+ return {
619
+ content: [{
620
+ type: "text",
621
+ text: safeStringify({ tables: tableNames.length, schema }, 2)
622
+ }]
623
+ };
624
+ }
625
+ );
626
+ server.tool(
627
+ "list_tables",
628
+ "List all available table/view names (short form of describe_schema).",
629
+ {},
630
+ async () => {
631
+ const db = getDb();
632
+ const tables = await dbAll(db, "SHOW TABLES");
633
+ const tableNames = tables.map((r) => r.name).sort();
218
634
  return {
219
635
  content: [{
220
636
  type: "text",
221
- text: JSON.stringify({
222
- source,
223
- target_z,
224
- count: filtered.length,
225
- rows: filtered
226
- }, null, 2)
637
+ text: safeStringify({ count: tableNames.length, tables: tableNames }, 2)
227
638
  }]
228
639
  };
229
640
  }
@@ -231,7 +642,13 @@ server.tool(
231
642
  var transport = new StdioServerTransport();
232
643
  await server.connect(transport);
233
644
  export {
234
- fetchParquetRows,
235
- getCatalog
645
+ ALLOWED_FIRST_WORDS,
646
+ BLOCKED_FUNCTIONS,
647
+ PKG_VERSION,
648
+ ensureCatalog,
649
+ getDb,
650
+ instructions,
651
+ libraryListPayload,
652
+ parseCatalog
236
653
  };
237
654
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { parquetRead } from \"hyparquet\";\nimport { compressors } from \"hyparquet-compressors\";\nimport { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nconst BASE_URL =\n process.env.NUCL_PARQUET_BASE_URL ??\n \"https://raw.githubusercontent.com/exoma-ch/nucl-parquet/main/data/\";\n\n// ---------------------------------------------------------------------------\n// Embedded catalog (from catalog.json)\n// ---------------------------------------------------------------------------\n\ninterface Library {\n name: string;\n description: string;\n source_url: string;\n projectiles: string[];\n data_type: string;\n version: string;\n path: string;\n}\n\ninterface Catalog {\n libraries: Record<string, Library>;\n shared: {\n meta: { path: string; files: Record<string, string> };\n stopping: { path: string; files: Record<string, string>; sources: string[] };\n };\n}\n\nconst CATALOG: Catalog = {\n libraries: {\n \"tendl-2024\": { name: \"TENDL-2024\", description: \"TALYS Evaluated Nuclear Data Library 2024 (IAEA/PSI)\", source_url: \"https://tendl.web.psi.ch/tendl_2024/tendl2024.html\", projectiles: [\"p\", \"d\", \"t\", \"h\", \"a\"], data_type: \"cross_sections\", version: \"2024\", path: \"tendl-2024/xs/\" },\n \"endfb-8.1\": { name: \"ENDF/B-VIII.1\", description: \"US Evaluated Nuclear Data File (NNDC/BNL)\", source_url: \"https://www.nndc.bnl.gov/endf-b8.1/\", projectiles: [\"n\", \"p\", \"d\", \"t\", \"h\", \"a\"], data_type: \"cross_sections\", version: \"VIII.1\", path: \"endfb-8.1/xs/\" },\n \"jeff-4.0\": { name: \"JEFF-4.0\", description: \"Joint Evaluated Fission and Fusion File (NEA)\", source_url: \"https://www.oecd-nea.org/dbdata/jeff/\", projectiles: [\"n\", \"p\"], data_type: \"cross_sections\", version: \"4.0\", path: \"jeff-4.0/xs/\" },\n \"jendl-5\": { name: \"JENDL-5\", description: \"Japanese Evaluated Nuclear Data Library (JAEA)\", source_url: \"https://wwwndc.jaea.go.jp/jendl/j5/j5.html\", projectiles: [\"n\", \"p\", \"d\", \"a\"], data_type: \"cross_sections\", version: \"5\", path: \"jendl-5/xs/\" },\n \"tendl-2025\": { name: \"TENDL-2025\", description: \"TALYS Evaluated Nuclear Data Library 2025 (PSI)\", source_url: \"https://tendl.web.psi.ch/\", projectiles: [\"n\", \"p\", \"d\", \"t\", \"h\", \"a\"], data_type: \"cross_sections\", version: \"2025\", path: \"tendl-2025/xs/\" },\n \"cendl-3.2\": { name: \"CENDL-3.2\", description: \"Chinese Evaluated Nuclear Data Library (CIAE)\", source_url: \"http://www.nuclear.csdb.cn/\", projectiles: [\"n\"], data_type: \"cross_sections\", version: \"3.2\", path: \"cendl-3.2/xs/\" },\n \"brond-3.1\": { name: \"BROND-3.1\", description: \"Russian Evaluated Nuclear Data Library (IPPE)\", source_url: \"https://vant.ippe.ru/\", projectiles: [\"n\"], data_type: \"cross_sections\", version: \"3.1\", path: \"brond-3.1/xs/\" },\n \"fendl-3.2\": { name: \"FENDL-3.2\", description: \"Fusion Evaluated Nuclear Data Library (IAEA)\", source_url: \"https://www-nds.iaea.org/fendl/\", projectiles: [\"n\"], data_type: \"cross_sections\", version: \"3.2\", path: \"fendl-3.2/xs/\" },\n \"eaf-2010\": { name: \"EAF-2010\", description: \"European Activation File (CCFE)\", source_url: \"https://fispact.ukaea.uk/\", projectiles: [\"n\"], data_type: \"cross_sections\", version: \"2010\", path: \"eaf-2010/xs/\" },\n \"irdff-2\": { name: \"IRDFF-II\", description: \"International Reactor Dosimetry and Fusion File (IAEA)\", source_url: \"https://www-nds.iaea.org/IRDFF/\", projectiles: [\"n\"], data_type: \"cross_sections\", version: \"II\", path: \"irdff-2/xs/\" },\n \"iaea-medical\": { name: \"IAEA-Medical\", description: \"Medical isotope production cross-sections (IAEA)\", source_url: \"https://www-nds.iaea.org/medical/\", projectiles: [\"p\", \"d\", \"h\", \"a\"], data_type: \"cross_sections\", version: \"latest\", path: \"iaea-medical/xs/\" },\n \"jendl-ad-2017\": { name: \"JENDL/AD-2017\", description: \"Activation/Dosimetry Library (JAEA)\", source_url: \"https://wwwndc.jaea.go.jp/jendl/jad/jad.html\", projectiles: [\"n\", \"p\"], data_type: \"cross_sections\", version: \"2017\", path: \"jendl-ad-2017/xs/\" },\n \"jendl-deu-2020\": { name: \"JENDL-DEU-2020\", description: \"Dedicated deuteron-induced reaction library (JAEA)\", source_url: \"https://wwwndc.jaea.go.jp/jendl/deu/deu.html\", projectiles: [\"d\"], data_type: \"cross_sections\", version: \"2020\", path: \"jendl-deu-2020/xs/\" },\n \"iaea-pd-2019\": { name: \"IAEA-PD-2019\", description: \"Photonuclear Data Library (IAEA)\", source_url: \"https://www-nds.iaea.org/photonuclear/\", projectiles: [\"g\"], data_type: \"cross_sections\", version: \"2019\", path: \"iaea-pd-2019/xs/\" },\n \"exfor\": { name: \"EXFOR\", description: \"Experimental nuclear reaction data (IAEA NDS)\", source_url: \"https://www-nds.iaea.org/exfor/\", projectiles: [\"n\", \"p\", \"d\", \"t\", \"h\", \"a\"], data_type: \"experimental_cross_sections\", version: \"latest\", path: \"exfor/\" },\n },\n shared: {\n meta: { path: \"meta/\", files: { abundances: \"abundances.parquet\", decay: \"decay.parquet\", elements: \"elements.parquet\" } },\n stopping: {\n path: \"stopping/\",\n files: {\n PSTAR: \"PSTAR.parquet\",\n ASTAR: \"ASTAR.parquet\",\n ESTAR: \"ESTAR.parquet\",\n dSTAR: \"dSTAR.parquet\",\n tSTAR: \"tSTAR.parquet\",\n catima: \"catima/catima.parquet\",\n } as Record<string, string>,\n sources: [\"PSTAR\", \"ASTAR\", \"ESTAR\", \"dSTAR\", \"tSTAR\", \"catima\"],\n },\n },\n};\n\n// ---------------------------------------------------------------------------\n// Parquet fetch + cache\n// ---------------------------------------------------------------------------\n\nconst parquetCache = new Map<string, Record<string, unknown>[]>();\n\nexport async function fetchParquetRows(\n relativePath: string,\n baseUrl: string = BASE_URL,\n): Promise<Record<string, unknown>[]> {\n if (parquetCache.has(relativePath)) return parquetCache.get(relativePath)!;\n\n const url = new URL(relativePath, baseUrl).href;\n const resp = await fetch(url);\n if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching ${url}`);\n const buffer = await resp.arrayBuffer();\n\n const rows: Record<string, unknown>[] = [];\n await parquetRead({\n file: buffer,\n compressors,\n rowFormat: \"object\",\n onComplete: (data: Record<string, unknown>[]) => {\n rows.push(...data);\n },\n });\n\n parquetCache.set(relativePath, rows);\n return rows;\n}\n\nasync function fetchManifest(libraryId: string): Promise<Record<string, unknown>> {\n const lib = CATALOG.libraries[libraryId];\n if (!lib) throw new Error(`Unknown library: ${libraryId}`);\n const manifestPath = lib.path.replace(/xs\\/$/, \"manifest.json\");\n const url = new URL(manifestPath, BASE_URL).href;\n const resp = await fetch(url);\n if (!resp.ok) throw new Error(`HTTP ${resp.status} fetching manifest for ${libraryId}`);\n return (await resp.json()) as Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nexport function getCatalog(): Catalog {\n return CATALOG;\n}\n\nfunction truncateRows(rows: Record<string, unknown>[], limit: number): { rows: Record<string, unknown>[]; truncated: boolean; total: number } {\n if (rows.length <= limit) return { rows, truncated: false, total: rows.length };\n return { rows: rows.slice(0, limit), truncated: true, total: rows.length };\n}\n\n// ---------------------------------------------------------------------------\n// MCP Server\n// ---------------------------------------------------------------------------\n\nconst server = new McpServer({\n name: \"nucl-parquet\",\n version: \"0.3.7\",\n});\n\nserver.tool(\n \"list_libraries\",\n \"List all available nuclear data libraries with projectiles and descriptions\",\n {},\n async () => {\n const libs = Object.entries(CATALOG.libraries).map(([id, lib]) => ({\n id,\n name: lib.name,\n description: lib.description,\n projectiles: lib.projectiles,\n version: lib.version,\n data_type: lib.data_type,\n }));\n return { content: [{ type: \"text\" as const, text: JSON.stringify(libs, null, 2) }] };\n },\n);\n\nserver.tool(\n \"list_isotopes\",\n \"List available target elements for a given library and projectile. Returns element symbols.\",\n {\n library: z.string().describe(\"Library ID, e.g. 'tendl-2024', 'endfb-8.1'\"),\n projectile: z.string().describe(\"Projectile: n, p, d, t, h, a, g\"),\n },\n async ({ library, projectile }) => {\n const lib = CATALOG.libraries[library];\n if (!lib) throw new Error(`Unknown library: ${library}. Use list_libraries to see available libraries.`);\n if (!lib.projectiles.includes(projectile)) {\n throw new Error(`Projectile '${projectile}' not available for ${library}. Available: ${lib.projectiles.join(\", \")}`);\n }\n\n const manifest = await fetchManifest(library);\n const elements = (manifest.elements as string[]) ?? [];\n\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ library, projectile, elements, count: elements.length }, null, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_cross_sections\",\n \"Get nuclear reaction cross-section data for a specific target element. Returns energy (MeV) and cross-section (mb) arrays with reaction product info.\",\n {\n library: z.string().describe(\"Library ID, e.g. 'tendl-2024'\"),\n projectile: z.string().describe(\"Projectile: n, p, d, t, h, a, g\"),\n element: z.string().describe(\"Target element symbol, e.g. 'Cu', 'Fe', 'Au'\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ library, projectile, element, max_rows }) => {\n const lib = CATALOG.libraries[library];\n if (!lib) throw new Error(`Unknown library: ${library}`);\n\n const parquetPath = `${lib.path}${projectile}_${element}.parquet`;\n const rows = await fetchParquetRows(parquetPath);\n const result = truncateRows(rows, max_rows ?? 500);\n\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({\n library,\n projectile,\n element,\n ...result,\n }, null, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_decay_data\",\n \"Get radioactive decay data (half-lives, decay modes, daughters) for a nuclide or element. Filter by Z and/or A.\",\n {\n z: z.number().optional().describe(\"Atomic number (e.g. 92 for U)\"),\n a: z.number().optional().describe(\"Mass number (e.g. 238)\"),\n },\n async ({ z: zNum, a: aNum }) => {\n if (zNum === undefined && aNum === undefined) {\n throw new Error(\"Provide at least z or a to filter decay data.\");\n }\n\n const metaPath = CATALOG.shared.meta.path + CATALOG.shared.meta.files.decay;\n const rows = await fetchParquetRows(metaPath);\n\n const filtered = rows.filter((row) => {\n if (zNum !== undefined && row[\"Z\"] !== zNum) return false;\n if (aNum !== undefined && row[\"A\"] !== aNum) return false;\n return true;\n });\n\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ z: zNum, a: aNum, count: filtered.length, rows: filtered }, null, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_abundances\",\n \"Get natural isotope abundances and atomic masses for an element.\",\n {\n z: z.number().describe(\"Atomic number (e.g. 29 for Cu)\"),\n },\n async ({ z: zNum }) => {\n const metaPath = CATALOG.shared.meta.path + CATALOG.shared.meta.files.abundances;\n const rows = await fetchParquetRows(metaPath);\n\n const filtered = rows.filter((row) => row[\"Z\"] === zNum);\n\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ z: zNum, count: filtered.length, isotopes: filtered }, null, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_stopping_power\",\n \"Get mass stopping power (dE/dx) data for a projectile in a target element.\",\n {\n source: z.string().describe(\n \"Data source: PSTAR (protons), ASTAR (α, NIST ICRU-49), ESTAR (electrons), dSTAR, tSTAR (velocity-scaled from PSTAR), or catima (full Z×Z table; α/³He fallback for non-NIST elements)\",\n ),\n target_z: z.number().describe(\"Target element atomic number\"),\n },\n async ({ source, target_z }) => {\n // Post-#143: aggregate stopping.parquet is deleted; route per-source.\n const fileMap = CATALOG.shared.stopping.files;\n if (!(source in fileMap)) {\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({\n error: `unknown source ${source}; valid: ${CATALOG.shared.stopping.sources.join(\", \")}`,\n }, null, 2),\n }],\n };\n }\n const stoppingPath = CATALOG.shared.stopping.path + fileMap[source];\n const rows = await fetchParquetRows(stoppingPath);\n\n // catima uses (proj_Z, target_Z); NIST tables use (source, target_Z).\n // Filter by target_Z in both cases — the file already restricts to one source.\n const filtered = rows.filter((row) => row[\"target_Z\"] === target_z);\n\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({\n source,\n target_z,\n count: filtered.length,\n rows: filtered,\n }, null, 2),\n }],\n };\n },\n);\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * nucl-parquet MCP server — wraps local data via DuckDB.\n *\n * SSoT refactor (epic #173, Sub-D #178): the MCP is a thin shell over\n * DuckDB-backed Parquet views, not a re-implementation of parquet I/O.\n * All data is read from the local data directory — no HTTP fetching.\n */\n\nimport { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport duckdb from \"duckdb\";\nimport { z } from \"zod\";\n\nconst require = createRequire(import.meta.url);\nexport const { version: PKG_VERSION } = require(\"../package.json\") as { version: string };\n\n// ---------------------------------------------------------------------------\n// Data directory resolution\n// ---------------------------------------------------------------------------\n\nfunction resolveDataDir(): string {\n // 1. Explicit env var\n const envDir = process.env.NUCL_PARQUET_DATA;\n if (envDir && existsSync(envDir)) return resolve(envDir);\n\n // 2. Repo-local data/ (development)\n const repoDir = resolve(import.meta.dirname ?? \".\", \"..\", \"..\", \"..\", \"..\", \"data\");\n if (existsSync(join(repoDir, \"catalog.json\"))) return repoDir;\n\n // 3. ~/.nucl-parquet/ (installed data)\n const homeData = join(homedir(), \".nucl-parquet\");\n if (existsSync(join(homeData, \"catalog.json\"))) return homeData;\n\n throw new Error(\n \"Cannot find nucl-parquet data directory. Set NUCL_PARQUET_DATA env var \" +\n \"or place data at ~/.nucl-parquet/. Data releases: \" +\n \"https://github.com/exoma-ch/nucl-parquet/releases\",\n );\n}\n\n// ---------------------------------------------------------------------------\n// Catalog (loaded from disk, not hardcoded)\n// ---------------------------------------------------------------------------\n\ninterface Library {\n name: string;\n description: string;\n source_url?: string;\n projectiles?: string[];\n data_type?: string;\n version: string;\n path?: string;\n}\n\ninterface ViewDef {\n path: string;\n type?: \"file\" | \"glob\";\n optional?: boolean;\n note?: string;\n}\n\ninterface Catalog {\n /**\n * The data release this server is serving, e.g. `2026.8.3`.\n *\n * Distinct from `Library.version`, which is the *evaluation's* version\n * (\"2023-iso\"). They answer different questions, and conflating them would\n * make a cross-server check silently wrong rather than merely absent (#348).\n *\n * Read from the catalog on disk, never compiled in — a build-time constant\n * would be a second source of truth for the one fact whose whole job is to\n * identify the data actually being read.\n */\n data_version: string;\n libraries: Record<string, Library>;\n views?: Record<string, ViewDef>;\n [key: string]: unknown;\n}\n\nlet _catalog: Catalog | undefined;\n\nexport function ensureCatalog(): Catalog {\n if (_catalog) return _catalog;\n const dataDir = resolveDataDir();\n _catalog = parseCatalog(readFileSync(join(dataDir, \"catalog.json\"), \"utf-8\"), dataDir);\n return _catalog;\n}\n\n/**\n * Parse and validate a catalog.\n *\n * Separate from {@link ensureCatalog} because that one memoises a module-level\n * singleton, which a test cannot re-enter — so the validation below would be\n * unreachable from a test and, in practice, unverified.\n */\nexport function parseCatalog(raw: string, dataDir: string): Catalog {\n const parsed = JSON.parse(raw) as Catalog;\n // `as Catalog` is a claim, not a check. `data_version` is required by\n // data/catalog.schema.json, and the whole point of reporting it is that it\n // identifies the tree actually being read — so an absent one must fail here\n // rather than surface as `undefined` in a referral an agent is trying to\n // verify (#348). The Rust server refuses the same catalog, for the same\n // reason.\n if (typeof parsed.data_version !== \"string\" || parsed.data_version === \"\") {\n throw new Error(\n `catalog.json at ${dataDir} has no 'data_version'. It is required by ` +\n `data/catalog.schema.json and identifies the data release this server serves; ` +\n `refusing to start rather than report an unknown release as if it were known.`,\n );\n }\n return parsed;\n}\n\n/**\n * The `list_libraries` payload: the data release, then the libraries.\n *\n * A named function rather than an inline object so a test can assert the shape\n * the tool actually returns. Asserting only that the catalog *has* a\n * data_version would pass while the tool still returned a bare array.\n */\nexport function libraryListPayload(cat: Catalog): {\n data_version: string;\n libraries: Array<Record<string, unknown>>;\n} {\n return {\n // Beside the array rather than on each entry, so the release and a\n // library's evaluation `version` cannot be read as the same kind of thing.\n data_version: cat.data_version,\n libraries: Object.entries(cat.libraries)\n .filter(([, lib]) => lib.projectiles)\n .map(([id, lib]) => ({\n id,\n name: lib.name,\n description: lib.description,\n projectiles: lib.projectiles,\n version: lib.version,\n data_type: lib.data_type,\n })),\n };\n}\n\n// ---------------------------------------------------------------------------\n// DuckDB connection (lazy init, thread-safe via singleton)\n// ---------------------------------------------------------------------------\n\nlet _db: duckdb.Database | undefined;\n\nfunction hasParquetFiles(dir: string): boolean {\n if (!existsSync(dir)) return false;\n try {\n return readdirSync(dir).some((f) => f.endsWith(\".parquet\"));\n } catch {\n return false;\n }\n}\n\nfunction registerParquet(db: duckdb.Database, filePath: string, viewName: string): void {\n if (existsSync(filePath)) {\n db.run(`CREATE VIEW ${viewName} AS SELECT * FROM read_parquet('${filePath}')`);\n }\n}\n\nfunction registerGlob(db: duckdb.Database, dir: string, viewName: string): void {\n if (hasParquetFiles(dir)) {\n const glob = join(dir, \"*.parquet\");\n db.run(`CREATE VIEW ${viewName} AS SELECT * FROM read_parquet('${glob}')`);\n }\n}\n\nfunction registerViews(db: duckdb.Database, dataDir: string): void {\n const catalog = ensureCatalog();\n\n // --- Cross-section libraries ---\n const libViews: string[] = [];\n for (const [libKey, lib] of Object.entries(catalog.libraries)) {\n if (!lib.path) continue;\n const libDir = join(dataDir, lib.path);\n if (!hasParquetFiles(libDir)) continue;\n\n const viewName = libKey.replace(/-/g, \"_\").replace(/\\./g, \"_\");\n const glob = join(libDir, \"*.parquet\");\n db.run(\n `CREATE VIEW ${viewName} AS SELECT *, '${libKey}' AS library FROM read_parquet('${glob}', filename=true)`,\n );\n if (lib.data_type === \"cross_sections\") libViews.push(viewName);\n }\n if (libViews.length > 0) {\n const union = libViews.map((v) => `SELECT * FROM ${v}`).join(\" UNION ALL \");\n db.run(`CREATE VIEW xs AS ${union}`);\n }\n\n // --- Catalog-driven view registration ---\n // All views declared in catalog.json::views — single source of truth.\n // New data tables become queryable by adding an entry to catalog.json,\n // no code changes needed in any client (Python, TypeScript, Rust).\n for (const [viewName, viewDef] of Object.entries(catalog.views ?? {})) {\n const viewPath = join(dataDir, viewDef.path);\n if (viewDef.type === \"glob\") {\n registerGlob(db, viewPath, viewName);\n } else {\n registerParquet(db, viewPath, viewName);\n }\n }\n\n // --- Special views that need logic beyond simple registration ---\n\n // ground_states: when nuclides.parquet exists, override with filtered view\n const nuclidesPath = join(dataDir, \"meta\", \"ensdf\", \"nuclides.parquet\");\n if (existsSync(nuclidesPath)) {\n // `'g'`, not `''`: #380 gave `state` one vocabulary and nuclides.parquet\n // now reads g=3148 / m=739 / m2=82 / m3=7 / null=13, with no `''` at all.\n // This view was silently empty against the rebuilt data.\n db.run(\"CREATE OR REPLACE VIEW ground_states AS SELECT * FROM nuclides WHERE state = 'g'\");\n }\n\n // EADL aliases: eadl_transitions (v0.11 compat) + fluorescence (radiative subset)\n const eadlDir = join(dataDir, \"meta\", \"eadl\");\n if (hasParquetFiles(eadlDir)) {\n db.run(\"CREATE VIEW eadl_transitions AS SELECT * FROM atomic_relaxation\");\n db.run(\"CREATE VIEW fluorescence AS SELECT * FROM atomic_relaxation WHERE transition_type = 'radiative'\");\n }\n}\n\n/**\n * Confine the connection to reading the data tree, then freeze its settings.\n *\n * `sql_query` hands callers a SQL prompt, and its keyword blocklist cannot list\n * every DuckDB function that opens a file: `sniff_csv('/etc/passwd')` and\n * `read_json_objects(...)` both got past it and read outside the data tree. The\n * connection itself must not be able to, whatever the SQL says:\n *\n * - `allowed_directories` first, because the views read Parquet files, which is\n * itself external access; without the exception every tool fails.\n * - `enable_external_access = false`: no other files, no network, no INSTALL.\n * - extension auto-install and auto-load off.\n * - `lock_configuration` last, so a caller cannot `SET` any of it back.\n *\n * Queued on the same connection after `registerViews`, so it takes effect\n * before the first caller query.\n */\nfunction harden(db: duckdb.Database, dataDir: string): void {\n const quoted = dataDir.replaceAll(\"'\", \"''\");\n db.run(`SET allowed_directories = ['${quoted}']`);\n db.run(\"SET enable_external_access = false\");\n db.run(\"SET autoinstall_known_extensions = false\");\n db.run(\"SET autoload_known_extensions = false\");\n db.run(\"SET lock_configuration = true\");\n}\n\nexport function getDb(): duckdb.Database {\n if (_db) return _db;\n const dataDir = resolveDataDir();\n const db = new duckdb.Database(\":memory:\");\n registerViews(db, dataDir);\n harden(db, dataDir);\n _db = db;\n return db;\n}\n\n// ---------------------------------------------------------------------------\n// DuckDB async helpers\n// ---------------------------------------------------------------------------\n\nfunction dbAll(db: duckdb.Database, sql: string, params?: unknown[]): Promise<Record<string, unknown>[]> {\n return new Promise((resolve, reject) => {\n const cb = (err: Error | null, rows: Record<string, unknown>[]) => {\n if (err) reject(err);\n else resolve(rows);\n };\n if (params && params.length > 0) {\n db.all(sql, ...params, cb);\n } else {\n db.all(sql, cb);\n }\n });\n}\n\nfunction dbRun(db: duckdb.Database, sql: string): Promise<void> {\n return new Promise((resolve, reject) => {\n db.run(sql, (err: Error | null) => {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n\n/** Serialize rows to JSON, converting BigInt values to Number. */\nfunction safeStringify(obj: unknown, indent?: number): string {\n return JSON.stringify(\n obj,\n (_key, value) => (typeof value === \"bigint\" ? Number(value) : value),\n indent,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Query helper\n// ---------------------------------------------------------------------------\n\nasync function query(\n sql: string,\n params?: unknown[],\n maxRows: number = 500,\n): Promise<{ total: number; truncated: boolean; rows: Record<string, unknown>[] }> {\n const db = getDb();\n // Get total count via subquery\n const countSql = `SELECT CAST(COUNT(*) AS INTEGER) AS n FROM (${sql})`;\n const countRows = await dbAll(db, countSql, params);\n const total = (countRows[0]?.n as number) ?? 0;\n\n // Get limited rows\n const limitSql = `${sql} LIMIT ${maxRows}`;\n const rows = await dbAll(db, limitSql, params);\n return { total, truncated: total > maxRows, rows };\n}\n\n// ---------------------------------------------------------------------------\n// MCP Server\n// ---------------------------------------------------------------------------\n\n/**\n * Server instructions, naming the data release in the client's context.\n *\n * The referral case #348 is about does not go through `list_libraries`:\n * hyrr-mcp sends an agent straight to `get_cross_sections` for full σ(E)\n * curves, having stated the release *it* computed against. Reporting the\n * release only from a tool the agent has no reason to call would leave that\n * claim as unverifiable as it was — so it is stated here too, where it reaches\n * the model without a round trip, and in `list_libraries` where it can be read\n * programmatically. Both read the same loaded catalog, so they cannot disagree.\n *\n * Kept word-for-word in step with the Rust server's `instructions()`; a test\n * pins that the two do not drift.\n */\nexport function instructions(dataVersion: string): string {\n return (\n `Serving nucl-parquet data release ${dataVersion}. This identifies the *data*, ` +\n `not this server's version — report it whenever you compare results with another ` +\n `server or tool. If another source states a different release, say so: agreement ` +\n `or disagreement computed across two releases is an artefact of the mismatch, not ` +\n `a physics result. Call list_libraries to read the same value programmatically.`\n );\n}\n\nconst server = new McpServer(\n {\n name: \"nucl-parquet\",\n // The package version — the software, not the data. Both are reported\n // because they answer different questions, and only one of them changes\n // when the data does.\n version: PKG_VERSION,\n },\n { instructions: instructions(ensureCatalog().data_version) },\n);\n\n// ---------------------------------------------------------------------------\n// Library / cross-section tools\n// ---------------------------------------------------------------------------\n\nserver.tool(\n \"list_libraries\",\n \"List all available nuclear data libraries with projectiles and descriptions\",\n {},\n async () => {\n // An envelope, not a bare array: `data_version` identifies the data\n // *release* these libraries came out of, a different fact from any one\n // library's evaluation `version`, and it has nowhere else to live (#348).\n return {\n content: [\n { type: \"text\" as const, text: safeStringify(libraryListPayload(ensureCatalog()), 2) },\n ],\n };\n },\n);\n\nserver.tool(\n \"list_isotopes\",\n \"List available target elements for a given library and projectile. Returns element symbols.\",\n {\n library: z.string().describe(\"Library ID, e.g. 'tendl-2025', 'endfb-8.1'\"),\n projectile: z.string().describe(\"Projectile: n, p, d, t, h, a, g\"),\n },\n async ({ library, projectile }) => {\n const cat = ensureCatalog();\n const lib = cat.libraries[library];\n if (!lib) throw new Error(`Unknown library: ${library}. Use list_libraries to see available libraries.`);\n if (!lib.projectiles?.includes(projectile)) {\n throw new Error(`Projectile '${projectile}' not available for ${library}. Available: ${lib.projectiles?.join(\", \") ?? \"none\"}`);\n }\n\n const dataDir = resolveDataDir();\n const libPath = lib.path;\n if (!libPath?.endsWith(\"xs/\")) {\n throw new Error(`Library ${library} path does not end with 'xs/' — cannot derive manifest path`);\n }\n const manifestPath = join(dataDir, libPath.replace(\"xs/\", \"manifest.json\"));\n if (!existsSync(manifestPath)) {\n throw new Error(`Manifest not found at ${manifestPath}`);\n }\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf-8\")) as { elements?: string[] };\n const elements = manifest.elements ?? [];\n\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ library, projectile, elements, count: elements.length }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_cross_sections\",\n \"Get nuclear reaction cross-section data for a specific target element. Returns energy (MeV) and cross-section (mb) arrays with reaction product info.\",\n {\n library: z.string().describe(\"Library ID, e.g. 'tendl-2025'\"),\n projectile: z.string().describe(\"Projectile: n, p, d, t, h, a, g\"),\n element: z.string().describe(\"Target element symbol, e.g. 'Cu', 'Fe', 'Au'\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ library, projectile, element, max_rows }) => {\n if (!/^[npdthag]$/.test(projectile)) {\n throw new Error(`Invalid projectile: '${projectile}'. Must be one of: n, p, d, t, h, a, g`);\n }\n if (!/^[A-Z][a-z]?$/.test(element)) {\n throw new Error(`Invalid element symbol: '${element}'. Must be 1-2 letters (e.g. 'Cu', 'Fe')`);\n }\n\n const cat = ensureCatalog();\n const lib = cat.libraries[library];\n if (!lib) throw new Error(`Unknown library: ${library}`);\n\n const dataDir = resolveDataDir();\n const parquetPath = join(dataDir, `${lib.path}${projectile}_${element}.parquet`);\n if (!existsSync(parquetPath)) {\n throw new Error(`No data for ${projectile}_${element} in ${library}`);\n }\n\n const db = getDb();\n const result = await query(\n `SELECT * FROM read_parquet('${parquetPath}')`,\n [],\n max_rows ?? 500,\n );\n\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ library, projectile, element, ...result }, 2),\n }],\n };\n },\n);\n\n// ---------------------------------------------------------------------------\n// Nuclear structure tools (DuckDB views)\n// ---------------------------------------------------------------------------\n\nserver.tool(\n \"get_decay_data\",\n \"Get radioactive decay data (half-lives, decay modes, daughters) for a nuclide or element. Filter by Z and/or A.\",\n {\n z: z.number().optional().describe(\"Atomic number (e.g. 92 for U)\"),\n a: z.number().optional().describe(\"Mass number (e.g. 238)\"),\n },\n async ({ z: zNum, a: aNum }) => {\n if (zNum === undefined && aNum === undefined) {\n throw new Error(\"Provide at least z or a to filter decay data.\");\n }\n const conditions: string[] = [];\n const params: unknown[] = [];\n if (zNum !== undefined) { conditions.push(\"Z = ?\"); params.push(zNum); }\n if (aNum !== undefined) { conditions.push(\"A = ?\"); params.push(aNum); }\n\n const result = await query(\n `SELECT * FROM decay WHERE ${conditions.join(\" AND \")}`,\n params,\n );\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ z: zNum, a: aNum, count: result.total, rows: result.rows }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_abundances\",\n \"Get natural isotope abundances and atomic masses for an element.\",\n {\n z: z.number().describe(\"Atomic number (e.g. 29 for Cu)\"),\n },\n async ({ z: zNum }) => {\n const result = await query(\"SELECT * FROM abundances WHERE Z = ?\", [zNum]);\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ z: zNum, count: result.total, isotopes: result.rows }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_stopping_power\",\n \"Get mass stopping power (dE/dx) data for a projectile in a target element.\",\n {\n source: z.string().describe(\n \"Data source: PSTAR (protons), ASTAR (α, NIST ICRU-49), ESTAR (electrons), dSTAR, tSTAR (velocity-scaled from PSTAR), or catima (full Z×Z table)\",\n ),\n target_z: z.number().describe(\"Target element atomic number\"),\n },\n async ({ source, target_z }) => {\n const viewMap: Record<string, string> = {\n PSTAR: \"stopping\", ASTAR: \"stopping\", ESTAR: \"stopping\",\n dSTAR: \"stopping\", tSTAR: \"stopping\", catima: \"catima_stopping\",\n };\n if (!(source in viewMap)) {\n throw new Error(`Unknown source '${source}'. Valid: PSTAR, ASTAR, ESTAR, dSTAR, tSTAR, catima`);\n }\n const view = viewMap[source];\n const sql = source === \"catima\"\n ? `SELECT * FROM ${view} WHERE target_Z = ?`\n : `SELECT * FROM ${view} WHERE source = ? AND target_Z = ?`;\n const params = source === \"catima\" ? [target_z] : [source, target_z];\n\n const result = await query(sql, params);\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ source, target_z, count: result.total, rows: result.rows }, 2),\n }],\n };\n },\n);\n\n// ---------------------------------------------------------------------------\n// Radiation / coincidence / spectra tools\n// ---------------------------------------------------------------------------\n\nserver.tool(\n \"get_radiation\",\n \"Get radiation emissions (gammas, X-rays, Auger electrons, conversion electrons) for a nuclide.\",\n {\n z: z.number().describe(\"Atomic number of the parent nuclide\"),\n a: z.number().optional().describe(\"Mass number (omit for all isotopes of element Z)\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ z: zNum, a: aNum, max_rows }) => {\n const conditions = [\"Z = ?\"];\n const params: unknown[] = [zNum];\n if (aNum !== undefined) { conditions.push(\"A = ?\"); params.push(aNum); }\n\n const result = await query(\n `SELECT * FROM radiation WHERE ${conditions.join(\" AND \")}`,\n params,\n max_rows ?? 500,\n );\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ z: zNum, a: aNum, ...result }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_coincidences\",\n \"Get gamma-gamma and mixed-emission coincidence pairs for a nuclide. Includes beta/EC/X-ray/Auger/511 keV annihilation paired with gammas.\",\n {\n z: z.number().describe(\"Atomic number of the parent nuclide\"),\n a: z.number().optional().describe(\"Mass number (omit for all isotopes of element Z)\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ z: zNum, a: aNum, max_rows }) => {\n const conditions = [\"Z = ?\"];\n const params: unknown[] = [zNum];\n if (aNum !== undefined) { conditions.push(\"A = ?\"); params.push(aNum); }\n\n const result = await query(\n `SELECT * FROM coincidences WHERE ${conditions.join(\" AND \")}`,\n params,\n max_rows ?? 500,\n );\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ z: zNum, a: aNum, ...result }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_summing_partners\",\n \"Get ICC-corrected summing partners for HPGe true-coincidence-summing (TCS) corrections. Returns emission pairs with pre-computed icc_correction_factor and pure_emission_joint_intensity.\",\n {\n z: z.number().describe(\"Atomic number of the daughter nuclide (filing convention)\"),\n a: z.number().describe(\"Mass number\"),\n primary_energy_keV: z.number().optional().describe(\"Filter to pairs matching this energy on either side\"),\n tolerance_keV: z.number().optional().describe(\"Energy match tolerance in keV (default 0.5)\"),\n emission1_rad_type: z.string().optional().describe(\"Filter side 1: 'gamma', 'xray', 'auger'\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ z: zNum, a: aNum, primary_energy_keV, tolerance_keV, emission1_rad_type, max_rows }) => {\n const conditions = [\"Z = ?\", \"A = ?\"];\n const params: unknown[] = [zNum, aNum];\n const tol = tolerance_keV ?? 0.5;\n\n if (primary_energy_keV !== undefined) {\n conditions.push(\"(ABS(emission1_energy_keV - ?) < ? OR ABS(emission2_energy_keV - ?) < ?)\");\n params.push(primary_energy_keV, tol, primary_energy_keV, tol);\n }\n if (emission1_rad_type !== undefined) {\n conditions.push(\"emission1_rad_type = ?\");\n params.push(emission1_rad_type);\n }\n\n const result = await query(\n `SELECT * FROM summing_partners WHERE ${conditions.join(\" AND \")} ORDER BY pure_emission_joint_intensity DESC`,\n params,\n max_rows ?? 500,\n );\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ z: zNum, a: aNum, primary_energy_keV, ...result }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_emissions\",\n \"Get absolute per-decay photon emission intensities (NuDat-equivalent). Returns gamma emissions for a parent nuclide with absolute intensities (photon emission probability per decay, 0-100%). Filed by parent, not daughter.\",\n {\n parent_z: z.number().describe(\"Atomic number of the decaying parent (e.g. 27 for Co-60)\"),\n parent_a: z.number().describe(\"Mass number of the parent (e.g. 60 for Co-60)\"),\n parent_state: z.string().optional().describe(\"Nuclear state: '' (ground), 'm', 'm2'\"),\n decay_mode: z.string().optional().describe(\"Filter by decay mode: 'beta-', 'KshellEC', 'IT', etc.\"),\n energy_keV: z.number().optional().describe(\"Filter to gammas near this energy\"),\n tolerance_keV: z.number().optional().describe(\"Energy tolerance (default 0.5 keV)\"),\n min_intensity_pct: z.number().optional().describe(\"Minimum absolute intensity (%) to include\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ parent_z, parent_a, parent_state, decay_mode, energy_keV, tolerance_keV, min_intensity_pct, max_rows }) => {\n const conditions = [\"parent_Z = ?\", \"parent_A = ?\", \"parent_state = ?\"];\n const params: unknown[] = [parent_z, parent_a, parent_state ?? \"\"];\n const tol = tolerance_keV ?? 0.5;\n\n if (decay_mode !== undefined) {\n conditions.push(\"decay_mode = ?\");\n params.push(decay_mode);\n }\n if (energy_keV !== undefined) {\n conditions.push(\"ABS(energy_keV - ?) < ?\");\n params.push(energy_keV, tol);\n }\n if (min_intensity_pct !== undefined && min_intensity_pct > 0) {\n conditions.push(\"intensity_pct >= ?\");\n params.push(min_intensity_pct);\n }\n\n const result = await query(\n `SELECT * FROM emissions WHERE ${conditions.join(\" AND \")} ORDER BY intensity_pct DESC`,\n params,\n max_rows ?? 500,\n );\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ parent_z, parent_a, parent_state: parent_state ?? \"\", ...result }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_beta_spectrum\",\n \"Get the continuous beta-decay kinetic-energy spectrum for a nuclide. Returns pre-tabulated Fermi-function spectra (dN/dE normalized to 1).\",\n {\n z: z.number().describe(\"Atomic number of the parent nuclide\"),\n a: z.number().describe(\"Mass number of the parent nuclide\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ z: zNum, a: aNum, max_rows }) => {\n const result = await query(\n \"SELECT * FROM beta_spectra WHERE Z = ? AND A = ?\",\n [zNum, aNum],\n max_rows ?? 500,\n );\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ z: zNum, a: aNum, ...result }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_compound_compositions\",\n \"Get elemental compositions (weight fractions) for NIST XCOM standard materials. Useful for Bragg-additive cross-section calculations.\",\n {\n material: z.string().optional().describe(\"Material key (e.g. 'water', 'air', 'concrete'). Omit to list all materials.\"),\n },\n async ({ material }) => {\n const db = getDb();\n if (material === undefined) {\n const rows = await dbAll(db, \"SELECT DISTINCT material FROM compound_compositions ORDER BY material\");\n const materials = rows.map((r) => r.material as string);\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ count: materials.length, materials }, 2),\n }],\n };\n }\n const result = await query(\n \"SELECT * FROM compound_compositions WHERE material = ?\",\n [material],\n );\n if (result.total === 0) {\n const allRows = await dbAll(db, \"SELECT DISTINCT material FROM compound_compositions ORDER BY material\");\n const all = allRows.map((r) => r.material as string);\n throw new Error(`Unknown material: '${material}'. Available: ${all.join(\", \")}`);\n }\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ material, count: result.total, composition: result.rows }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"get_electron_stopping\",\n \"Get electron stopping power with collision/radiative split. Richer than ESTAR — includes ~183 compounds plus all elements Z=1..98.\",\n {\n target: z.string().optional().describe(\"Compound name (e.g. 'G4_WATER'). For elements use target_z instead.\"),\n target_z: z.number().optional().describe(\"Atomic number for elemental targets\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 500)\"),\n },\n async ({ target, target_z, max_rows }) => {\n if (target === undefined && target_z === undefined) {\n throw new Error(\"Provide target (compound name) or target_z (atomic number).\");\n }\n let result;\n if (target_z !== undefined) {\n result = await query(\n \"SELECT * FROM electron_stopping WHERE target_Z = ?\",\n [target_z],\n max_rows ?? 500,\n );\n } else {\n result = await query(\n \"SELECT * FROM electron_stopping WHERE name = ? OR g4_name = ?\",\n [target, target],\n max_rows ?? 500,\n );\n }\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ target, target_z, ...result }, 2),\n }],\n };\n },\n);\n\n// ---------------------------------------------------------------------------\n// SQL escape hatch (Sub-E #179)\n// ---------------------------------------------------------------------------\n\n// Patterns that indicate file-access functions — blocked in user SQL to prevent\n// read_parquet('/etc/passwd') style attacks. The pre-registered DuckDB views\n// already expose all nuclear data; there's no legitimate need for raw file access.\nexport const BLOCKED_FUNCTIONS = /\\b(read_parquet|parquet_scan|parquet_metadata|parquet_schema|read_csv|read_csv_auto|read_json|read_json_auto|read_text|read_blob|glob|copy|export|attach|load|install|create|drop|alter|insert|update|delete|truncate|query_table|pragma)\\b/i;\n\nexport const ALLOWED_FIRST_WORDS = new Set([\"SELECT\", \"WITH\", \"EXPLAIN\", \"DESCRIBE\", \"SHOW\", \"SUMMARIZE\"]);\n\nserver.tool(\n \"sql_query\",\n \"Execute read-only SQL against all 70+ nuclear data tables. Supports JOINs, aggregations, window functions. Use describe_schema() to discover available tables and columns.\",\n {\n sql: z.string().describe(\"Read-only SQL query. DDL/DML will be rejected.\"),\n max_rows: z.number().optional().describe(\"Max rows to return (default 10000)\"),\n },\n async ({ sql: userSql, max_rows }) => {\n const stripped = userSql.trim();\n if (!stripped) throw new Error(\"Empty SQL query\");\n\n const firstWord = stripped.split(/\\s/)[0].toUpperCase();\n if (!ALLOWED_FIRST_WORDS.has(firstWord)) {\n throw new Error(`Only read queries are allowed (SELECT, WITH, EXPLAIN, DESCRIBE). Got: ${firstWord}`);\n }\n if (BLOCKED_FUNCTIONS.test(stripped)) {\n throw new Error(\"Only read queries are allowed (SELECT, WITH, EXPLAIN, DESCRIBE). File-access and DDL functions are blocked.\");\n }\n\n const db = getDb();\n const limit = max_rows ?? 10000;\n let total: number;\n let rows: Record<string, unknown>[];\n try {\n // Get total count first, then fetch limited rows\n const countRows = await dbAll(db, `SELECT CAST(COUNT(*) AS INTEGER) AS n FROM (${stripped})`);\n total = (countRows[0]?.n as number) ?? 0;\n rows = await dbAll(db, `SELECT * FROM (${stripped}) LIMIT ${limit}`);\n } catch (e) {\n throw new Error(`SQL error: ${(e as Error).message}`);\n }\n\n const truncated = total > limit;\n const display = rows;\n\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ total, truncated, rows: display }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"describe_schema\",\n \"List all available tables/views with their column names and types. Use this to discover what data is available before writing SQL queries.\",\n {},\n async () => {\n const db = getDb();\n const tables = await dbAll(db, \"SHOW TABLES\");\n const tableNames = tables.map((r) => r.name as string).sort();\n\n const schema: Record<string, { name: string; type: string }[]> = {};\n for (const tbl of tableNames) {\n try {\n const cols = await dbAll(db, `DESCRIBE ${tbl}`);\n schema[tbl] = cols.map((c) => ({ name: c.column_name as string, type: c.column_type as string }));\n } catch {\n schema[tbl] = [];\n }\n }\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ tables: tableNames.length, schema }, 2),\n }],\n };\n },\n);\n\nserver.tool(\n \"list_tables\",\n \"List all available table/view names (short form of describe_schema).\",\n {},\n async () => {\n const db = getDb();\n const tables = await dbAll(db, \"SHOW TABLES\");\n const tableNames = tables.map((r) => r.name as string).sort();\n return {\n content: [{\n type: \"text\" as const,\n text: safeStringify({ count: tableNames.length, tables: tableNames }, 2),\n }],\n };\n },\n);\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n"],"mappings":"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","names":["require","resolve"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nucl-parquet/mcp",
3
- "version": "0.13.2",
4
- "description": "MCP server for nuclear data — lazy-loads Parquet from GitHub",
3
+ "version": "0.15.0",
4
+ "description": "MCP server for nuclear data — wraps local Parquet via DuckDB",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "nucl-parquet-mcp": "./dist/index.js"
@@ -12,7 +12,7 @@
12
12
  "dist"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsup",
15
+ "build": "tsup && tsc --emitDeclarationOnly",
16
16
  "test": "vitest run",
17
17
  "test:watch": "vitest",
18
18
  "start": "node dist/index.js"
@@ -20,15 +20,14 @@
20
20
  "repository": "https://github.com/exoma-ch/nucl-parquet",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
- "@modelcontextprotocol/sdk": "^1.12.0",
24
- "hyparquet": "^1.7.0",
25
- "hyparquet-compressors": "^1.1.0",
26
- "zod": "^3.23.0"
23
+ "@modelcontextprotocol/sdk": "^1.30.1",
24
+ "duckdb": "^1.4.4",
25
+ "zod": "^4.6.5"
27
26
  },
28
27
  "devDependencies": {
29
- "@types/node": "^22.0.0",
28
+ "@types/node": "^26.6.3",
30
29
  "tsup": "^8.0.0",
31
- "typescript": "^5.4.0",
32
- "vitest": "^2.0.0"
30
+ "typescript": "^7.0.2",
31
+ "vitest": "^5.0.2"
33
32
  }
34
33
  }