@mrfentmen/nastran-bdf-mcp 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # Nastran BDF MCP
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Nastran MCP: parse BDF cards, validate grids, elements and ENDDATA.
4
+
5
+ No network calls. Files are parsed locally and nothing leaves the machine.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx -y @mrfentmen/nastran-bdf-mcp
11
+ ```
12
+
13
+ Or in an MCP client:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "nastran-bdf-mcp": {
19
+ "command": "npx",
20
+ "args": ["-y", "@mrfentmen/nastran-bdf-mcp"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## Tools
27
+
28
+ | Tool | What it does |
29
+ |---|---|
30
+ | `detect_files` | Find Nastran BDF files in a directory tree. |
31
+ | `read_file` | Read a file or line range. |
32
+ | `write_file` | Write a file, with overwrite protection. |
33
+ | `parse` | Parse into records, fields and metadata. |
34
+ | `validate` | Check structural rules and report issues by line. |
35
+ | `summarize` | Record counts, kinds and document metadata. |
36
+ | `list_records` | List record ids and kinds. |
37
+ | `get_record` | Full fields for one record. |
38
+ | `search` | Plain text or regex search across records. |
39
+ | `stats` | Numeric min/max/mean per field. |
40
+ | `format_reference` | Compact spec reference. |
41
+ | `sample` | A small valid example document. |
42
+ | `diff` | Compare two documents. |
43
+ | `to_json` | Export every record as JSON. |
44
+
45
+ ## Safety
46
+
47
+ This server reads and writes local files. Treat file paths as trusted input.
48
+ Invalid documents are reported, never silently repaired.
49
+
50
+ ## Run locally
51
+
52
+ ```bash
53
+ npm install
54
+ npm run build
55
+ node dist/index.js
56
+ ```
package/dist/core.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { FormatModule } from "./types.js";
3
+ export declare function registerFormatTools(server: McpServer, format: FormatModule): void;
package/dist/core.js ADDED
@@ -0,0 +1,343 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { errorMessage, fail, text, ToolError } from "./run.js";
5
+ const MAX_INPUT = 4_000_000;
6
+ const SKIP_DIRS = new Set([".git", "node_modules", "dist", "build", ".mcp-build"]);
7
+ async function readInput(input) {
8
+ if (input.path && input.content !== undefined)
9
+ throw new ToolError("Use path or content, not both");
10
+ if (input.content !== undefined) {
11
+ if (input.content.length > MAX_INPUT)
12
+ throw new ToolError(`Input is larger than ${MAX_INPUT} characters`);
13
+ return { body: input.content };
14
+ }
15
+ if (!input.path)
16
+ throw new ToolError("Provide a path or content");
17
+ if (input.path.includes("\0"))
18
+ throw new ToolError("Path contains a null byte");
19
+ const file = path.isAbsolute(input.path) ? input.path : path.resolve(input.cwd ? path.resolve(input.cwd) : process.cwd(), input.path);
20
+ const stat = await fs.stat(file);
21
+ if (!stat.isFile())
22
+ throw new ToolError(`Not a file: ${file}`);
23
+ if (stat.size > MAX_INPUT)
24
+ throw new ToolError(`File is larger than ${MAX_INPUT} bytes`);
25
+ return { body: await fs.readFile(file, "utf8"), file };
26
+ }
27
+ function parseOrThrow(format, body) {
28
+ const parsed = format.parse(body);
29
+ const errors = parsed.issues.filter((issue) => issue.severity === "error");
30
+ if (errors.length > 0) {
31
+ const first = errors.slice(0, 5).map((issue) => (issue.line ? `line ${issue.line}: ${issue.message}` : issue.message));
32
+ throw new ToolError(`${format.displayName} could not be parsed:\n${first.join("\n")}`);
33
+ }
34
+ return parsed;
35
+ }
36
+ function numericStats(records) {
37
+ const out = {};
38
+ for (const record of records) {
39
+ for (const [key, value] of Object.entries(record.fields)) {
40
+ if (typeof value !== "number" || !Number.isFinite(value))
41
+ continue;
42
+ const current = out[key];
43
+ if (!current) {
44
+ out[key] = { count: 1, min: value, max: value, mean: value, sum: value };
45
+ }
46
+ else {
47
+ current.count += 1;
48
+ current.sum += value;
49
+ current.min = Math.min(current.min, value);
50
+ current.max = Math.max(current.max, value);
51
+ current.mean = current.sum / current.count;
52
+ }
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ function round(value) {
58
+ return Number.isFinite(value) ? Math.round(value * 1000) / 1000 : value;
59
+ }
60
+ export function registerFormatTools(server, format) {
61
+ const sourceArgs = {
62
+ path: z.string().min(1).max(2_000).optional(),
63
+ content: z.string().max(MAX_INPUT).optional(),
64
+ cwd: z.string().min(1).max(2_000).optional(),
65
+ };
66
+ server.tool("detect_files", `Find ${format.displayName} files (${format.extensions.join(", ")}) in a directory tree.`, {
67
+ path: z.string().min(1).max(2_000).default("."),
68
+ maxDepth: z.number().int().min(0).max(8).default(4),
69
+ }, async (a) => {
70
+ try {
71
+ const root = path.resolve(a.path);
72
+ const stat = await fs.stat(root);
73
+ if (!stat.isDirectory())
74
+ throw new ToolError(`Not a directory: ${root}`);
75
+ const found = [];
76
+ let scanned = 0;
77
+ async function walk(dir, depth) {
78
+ if (depth > a.maxDepth || found.length >= 500)
79
+ return;
80
+ let entries;
81
+ try {
82
+ entries = await fs.readdir(dir, { withFileTypes: true });
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ for (const entry of entries) {
88
+ if (entry.name.startsWith("."))
89
+ continue;
90
+ const full = path.join(dir, entry.name);
91
+ if (entry.isDirectory()) {
92
+ if (SKIP_DIRS.has(entry.name))
93
+ continue;
94
+ await walk(full, depth + 1);
95
+ continue;
96
+ }
97
+ if (!entry.isFile())
98
+ continue;
99
+ scanned += 1;
100
+ if (!format.extensions.includes(path.extname(entry.name).toLowerCase()))
101
+ continue;
102
+ found.push(path.relative(root, full));
103
+ if (found.length >= 500)
104
+ return;
105
+ }
106
+ }
107
+ await walk(root, 0);
108
+ return text(JSON.stringify({ format: format.displayName, root, filesScanned: scanned, matches: found.length, files: found }, null, 2));
109
+ }
110
+ catch (error) {
111
+ return fail(errorMessage(error));
112
+ }
113
+ });
114
+ server.tool("read_file", `Read a ${format.displayName} file, optionally a line range.`, {
115
+ path: z.string().min(1).max(2_000),
116
+ startLine: z.number().int().min(1).default(1),
117
+ endLine: z.number().int().min(1).optional(),
118
+ cwd: z.string().min(1).max(2_000).optional(),
119
+ }, async (a) => {
120
+ try {
121
+ const { body } = await readInput({ path: a.path, cwd: a.cwd });
122
+ const lines = body.split(/\r?\n/);
123
+ const end = Math.min(a.endLine ?? lines.length, lines.length);
124
+ return text(lines.slice(a.startLine - 1, end).join("\n"));
125
+ }
126
+ catch (error) {
127
+ return fail(errorMessage(error));
128
+ }
129
+ });
130
+ server.tool("write_file", `Write a ${format.displayName} file. Refuses to overwrite unless overwrite is true.`, {
131
+ path: z.string().min(1).max(2_000),
132
+ content: z.string().max(MAX_INPUT),
133
+ overwrite: z.boolean().default(false),
134
+ cwd: z.string().min(1).max(2_000).optional(),
135
+ }, async (a) => {
136
+ try {
137
+ const file = path.isAbsolute(a.path) ? a.path : path.resolve(a.cwd ? path.resolve(a.cwd) : process.cwd(), a.path);
138
+ if (!a.overwrite) {
139
+ const exists = await fs.stat(file).then(() => true).catch(() => false);
140
+ if (exists)
141
+ throw new ToolError(`File already exists: ${file}. Set overwrite=true to replace it.`);
142
+ }
143
+ await fs.mkdir(path.dirname(file), { recursive: true });
144
+ await fs.writeFile(file, a.content, "utf8");
145
+ return text(`Wrote ${Buffer.byteLength(a.content)} bytes to ${file}`);
146
+ }
147
+ catch (error) {
148
+ return fail(errorMessage(error));
149
+ }
150
+ });
151
+ server.tool("parse", `Parse ${format.displayName} into records, fields and document metadata.`, { ...sourceArgs }, async (a) => {
152
+ try {
153
+ const { body, file } = await readInput(a);
154
+ const parsed = parseOrThrow(format, body);
155
+ return text(JSON.stringify({
156
+ file: file ?? null,
157
+ format: parsed.format,
158
+ meta: parsed.meta,
159
+ records: parsed.records.length,
160
+ kinds: parsed.records.reduce((acc, record) => {
161
+ acc[record.kind] = (acc[record.kind] ?? 0) + 1;
162
+ return acc;
163
+ }, {}),
164
+ issues: parsed.issues,
165
+ }, null, 2));
166
+ }
167
+ catch (error) {
168
+ return fail(errorMessage(error));
169
+ }
170
+ });
171
+ server.tool("validate", `Validate ${format.displayName} against its structural rules and report every issue with a line number.`, { ...sourceArgs }, async (a) => {
172
+ try {
173
+ const { body } = await readInput(a);
174
+ const parsed = format.parse(body);
175
+ const errors = parsed.issues.filter((issue) => issue.severity === "error").length;
176
+ const warnings = parsed.issues.length - errors;
177
+ return text(JSON.stringify({
178
+ format: parsed.format,
179
+ valid: errors === 0,
180
+ errors,
181
+ warnings,
182
+ issues: parsed.issues,
183
+ }, null, 2));
184
+ }
185
+ catch (error) {
186
+ return fail(errorMessage(error));
187
+ }
188
+ });
189
+ server.tool("summarize", `Summarize a ${format.displayName} document: metadata, record counts and per-kind totals.`, { ...sourceArgs }, async (a) => {
190
+ try {
191
+ const { body } = await readInput(a);
192
+ const parsed = parseOrThrow(format, body);
193
+ const kinds = parsed.records.reduce((acc, record) => {
194
+ acc[record.kind] = (acc[record.kind] ?? 0) + 1;
195
+ return acc;
196
+ }, {});
197
+ const lines = [
198
+ `${format.displayName} document`,
199
+ `records: ${parsed.records.length}`,
200
+ ...Object.entries(kinds).map(([kind, count]) => ` ${kind}: ${count}`),
201
+ ...Object.entries(parsed.meta).map(([key, value]) => `${key}: ${String(value)}`),
202
+ `issues: ${parsed.issues.length}`,
203
+ ];
204
+ return text(lines.join("\n"));
205
+ }
206
+ catch (error) {
207
+ return fail(errorMessage(error));
208
+ }
209
+ });
210
+ server.tool("list_records", `List ${format.displayName} record ids and kinds, with a limit.`, {
211
+ ...sourceArgs,
212
+ limit: z.number().int().min(1).max(1_000).default(50),
213
+ }, async (a) => {
214
+ try {
215
+ const { body } = await readInput(a);
216
+ const parsed = parseOrThrow(format, body);
217
+ const rows = parsed.records.slice(0, a.limit).map((record) => `${record.kind}\t${record.id}`);
218
+ const rest = parsed.records.length - rows.length;
219
+ return text(rows.join("\n") + (rest > 0 ? `\n... ${rest} more` : ""));
220
+ }
221
+ catch (error) {
222
+ return fail(errorMessage(error));
223
+ }
224
+ });
225
+ server.tool("get_record", `Return one ${format.displayName} record by id, with all parsed fields.`, {
226
+ ...sourceArgs,
227
+ id: z.string().min(1).max(300),
228
+ }, async (a) => {
229
+ try {
230
+ const { body } = await readInput(a);
231
+ const parsed = parseOrThrow(format, body);
232
+ const record = parsed.records.find((item) => item.id === a.id);
233
+ if (!record)
234
+ throw new ToolError(`No record with id ${a.id}. Use list_records to see available ids.`);
235
+ return text(JSON.stringify(record, null, 2));
236
+ }
237
+ catch (error) {
238
+ return fail(errorMessage(error));
239
+ }
240
+ });
241
+ server.tool("search", `Search ${format.displayName} records by plain text or regular expression.`, {
242
+ ...sourceArgs,
243
+ query: z.string().min(1).max(500),
244
+ regex: z.boolean().default(false),
245
+ limit: z.number().int().min(1).max(500).default(50),
246
+ }, async (a) => {
247
+ try {
248
+ const { body } = await readInput(a);
249
+ const parsed = parseOrThrow(format, body);
250
+ let test;
251
+ if (a.regex) {
252
+ let pattern;
253
+ try {
254
+ pattern = new RegExp(a.query, "i");
255
+ }
256
+ catch {
257
+ throw new ToolError(`Invalid regular expression: ${a.query}`);
258
+ }
259
+ test = (value) => pattern.test(value);
260
+ }
261
+ else {
262
+ const needle = a.query.toLowerCase();
263
+ test = (value) => value.toLowerCase().includes(needle);
264
+ }
265
+ const matches = parsed.records.filter((record) => test(`${record.id} ${record.kind} ${JSON.stringify(record.fields)}`));
266
+ const rows = matches.slice(0, a.limit).map((record) => `${record.kind}\t${record.id}`);
267
+ return text(`${matches.length} match(es)\n${rows.join("\n")}`.trim());
268
+ }
269
+ catch (error) {
270
+ return fail(errorMessage(error));
271
+ }
272
+ });
273
+ server.tool("stats", `Numeric statistics for ${format.displayName} fields across all records.`, {
274
+ ...sourceArgs,
275
+ field: z.string().min(1).max(120).optional(),
276
+ }, async (a) => {
277
+ try {
278
+ const { body } = await readInput(a);
279
+ const parsed = parseOrThrow(format, body);
280
+ const stats = numericStats(parsed.records);
281
+ if (a.field) {
282
+ const one = stats[a.field];
283
+ if (!one)
284
+ throw new ToolError(`Field ${a.field} has no numeric values. Numeric fields: ${Object.keys(stats).join(", ") || "none"}`);
285
+ return text(JSON.stringify({ [a.field]: { ...one, mean: round(one.mean) } }, null, 2));
286
+ }
287
+ const rounded = Object.fromEntries(Object.entries(stats).map(([key, value]) => [key, { ...value, mean: round(value.mean) }]));
288
+ return text(JSON.stringify(rounded, null, 2));
289
+ }
290
+ catch (error) {
291
+ return fail(errorMessage(error));
292
+ }
293
+ });
294
+ server.tool("to_json", `Export every parsed ${format.displayName} record as JSON.`, { ...sourceArgs }, async (a) => {
295
+ try {
296
+ const { body, file } = await readInput(a);
297
+ const parsed = parseOrThrow(format, body);
298
+ return text(JSON.stringify({
299
+ file: file ?? null,
300
+ format: parsed.format,
301
+ meta: parsed.meta,
302
+ issues: parsed.issues,
303
+ records: parsed.records,
304
+ }, null, 2));
305
+ }
306
+ catch (error) {
307
+ return fail(errorMessage(error));
308
+ }
309
+ });
310
+ server.tool("diff", `Compare two ${format.displayName} documents: added, removed and changed records.`, {
311
+ leftPath: z.string().min(1).max(2_000).optional(),
312
+ leftContent: z.string().max(MAX_INPUT).optional(),
313
+ rightPath: z.string().min(1).max(2_000).optional(),
314
+ rightContent: z.string().max(MAX_INPUT).optional(),
315
+ cwd: z.string().min(1).max(2_000).optional(),
316
+ }, async (a) => {
317
+ try {
318
+ const left = await readInput({ path: a.leftPath, content: a.leftContent, cwd: a.cwd });
319
+ const right = await readInput({ path: a.rightPath, content: a.rightContent, cwd: a.cwd });
320
+ const before = parseOrThrow(format, left.body);
321
+ const after = parseOrThrow(format, right.body);
322
+ const beforeFields = new Map(before.records.map((record) => [record.id, JSON.stringify(record.fields)]));
323
+ const afterFields = new Map(after.records.map((record) => [record.id, JSON.stringify(record.fields)]));
324
+ const added = [...afterFields.keys()].filter((id) => !beforeFields.has(id));
325
+ const removed = [...beforeFields.keys()].filter((id) => !afterFields.has(id));
326
+ const changed = [...afterFields.keys()].filter((id) => beforeFields.has(id) && beforeFields.get(id) !== afterFields.get(id));
327
+ return text(JSON.stringify({
328
+ format: before.format,
329
+ added: added.length,
330
+ removed: removed.length,
331
+ changed: changed.length,
332
+ unchanged: afterFields.size - added.length - changed.length,
333
+ records: { added, removed, changed },
334
+ issues: { left: before.issues.length, right: after.issues.length },
335
+ }, null, 2));
336
+ }
337
+ catch (error) {
338
+ return fail(errorMessage(error));
339
+ }
340
+ });
341
+ server.tool("format_reference", `Return a compact ${format.displayName} reference: structure, required fields and common pitfalls.`, {}, async () => text(format.reference));
342
+ server.tool("sample", `Return a small but valid ${format.displayName} example document.`, {}, async () => text(format.sample()));
343
+ }
@@ -0,0 +1,2 @@
1
+ import type { FormatModule } from "./types.js";
2
+ export declare const format: FormatModule;
package/dist/format.js ADDED
@@ -0,0 +1,155 @@
1
+ const ELEMENTS = new Set(["CTRIA3", "CQUAD4", "CTETRA", "CHEXA", "CPENTA", "CBAR", "CBEAM", "CROD", "CONM2", "RBE2", "RBE3"]);
2
+ const PROPERTIES = new Set(["PSHELL", "PSOLID", "PBAR", "PBEAM", "PROD", "PCOMP", "MAT1", "MAT2", "MAT8"]);
3
+ function fields(line) {
4
+ if (line.includes(",")) {
5
+ const out = line.split(",").map((value) => value.trim());
6
+ while (out.length > 0 && out[out.length - 1] === "")
7
+ out.pop();
8
+ return out;
9
+ }
10
+ const out = [];
11
+ for (let index = 0; index < line.length; index += 8) {
12
+ const chunk = line.slice(index, index + 8).trim();
13
+ if (chunk.length === 0)
14
+ break;
15
+ if (chunk.startsWith("*"))
16
+ break;
17
+ out.push(chunk);
18
+ }
19
+ return out;
20
+ }
21
+ function parse(text) {
22
+ const issues = [];
23
+ const records = [];
24
+ const grids = new Map();
25
+ let sawEnd = false;
26
+ let cardCount = 0;
27
+ text.split(/\r?\n/).forEach((raw, index) => {
28
+ const number = index + 1;
29
+ const line = raw.trim();
30
+ if (line.length === 0)
31
+ return;
32
+ if (line.startsWith("$") || line.startsWith("BEGIN"))
33
+ return;
34
+ if (line === "ENDDATA") {
35
+ sawEnd = true;
36
+ return;
37
+ }
38
+ const parts = fields(line);
39
+ const card = (parts[0] ?? "").toUpperCase();
40
+ if (!/^[A-Z][A-Z0-9]{2,7}$/.test(card)) {
41
+ issues.push({ code: "bad-card", severity: "error", message: `Line does not start with a Nastran card name: ${line.slice(0, 40)}`, line: number });
42
+ return;
43
+ }
44
+ cardCount += 1;
45
+ if (card === "GRID") {
46
+ const id = Number(parts[1]);
47
+ const [x, y, z] = [Number(parts[3]), Number(parts[4]), Number(parts[5])];
48
+ if (!Number.isInteger(id) || id <= 0) {
49
+ issues.push({ code: "bad-grid-id", severity: "error", message: `GRID id must be a positive integer: ${parts[1]}`, line: number });
50
+ return;
51
+ }
52
+ if (grids.has(id)) {
53
+ issues.push({ code: "duplicate-grid", severity: "error", message: `GRID ${id} is defined twice`, line: number });
54
+ }
55
+ grids.set(id, number);
56
+ for (const value of [x, y, z]) {
57
+ if (value !== undefined && !Number.isNaN(value) && parts.length > 3 && !Number.isFinite(value)) {
58
+ issues.push({ code: "bad-coordinate", severity: "error", message: `GRID ${id} has a non numeric coordinate`, line: number });
59
+ break;
60
+ }
61
+ }
62
+ records.push({ id: `grid:${id}`, kind: "grid", fields: { id, x: x || 0, y: y || 0, z: z || 0, line: number } });
63
+ return;
64
+ }
65
+ if (ELEMENTS.has(card)) {
66
+ const id = Number(parts[1]);
67
+ const property = Number(parts[2]);
68
+ const refs = parts.slice(3).map(Number).filter((value) => Number.isFinite(value));
69
+ if (!Number.isInteger(id) || id <= 0) {
70
+ issues.push({ code: "bad-element-id", severity: "error", message: `${card} id must be a positive integer: ${parts[1]}`, line: number });
71
+ return;
72
+ }
73
+ records.push({
74
+ id: `${card}:${id}`,
75
+ kind: "element",
76
+ fields: { card, id, property, nodes: refs.length, connectivity: refs.join(","), line: number },
77
+ });
78
+ return;
79
+ }
80
+ if (PROPERTIES.has(card)) {
81
+ const id = Number(parts[1]);
82
+ if (!Number.isInteger(id) || id <= 0) {
83
+ issues.push({ code: "bad-property-id", severity: "error", message: `${card} id must be a positive integer: ${parts[1]}`, line: number });
84
+ }
85
+ records.push({ id: `${card}:${id || number}`, kind: "property", fields: { card, id, fields: parts.length, line: number } });
86
+ return;
87
+ }
88
+ records.push({ id: `${card}:${number}`, kind: "card", fields: { card, fields: parts.length, line: number } });
89
+ });
90
+ if (!sawEnd) {
91
+ issues.push({ code: "missing-enddata", severity: "error", message: "File has no ENDDATA card" });
92
+ }
93
+ if (grids.size === 0) {
94
+ issues.push({ code: "no-grids", severity: "warning", message: "Model defines no GRID points" });
95
+ }
96
+ for (const record of records) {
97
+ if (record.kind !== "element")
98
+ continue;
99
+ const refs = String(record.fields.connectivity ?? "").split(",").filter(Boolean).map(Number);
100
+ for (const ref of refs) {
101
+ if (!grids.has(ref)) {
102
+ issues.push({
103
+ code: "unknown-grid",
104
+ severity: "warning",
105
+ message: `${record.fields.card} ${record.fields.id} references GRID ${ref}, which is not defined`,
106
+ });
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ return {
112
+ format: "Nastran BDF",
113
+ records,
114
+ meta: {
115
+ cards: cardCount,
116
+ grids: grids.size,
117
+ elements: records.filter((record) => record.kind === "element").length,
118
+ properties: records.filter((record) => record.kind === "property").length,
119
+ format: text.includes(",") ? "free" : "fixed",
120
+ },
121
+ issues,
122
+ };
123
+ }
124
+ const reference = [
125
+ "Nastran bulk data file (BDF)",
126
+ "",
127
+ "GRID,1,,0.0,0.0,0.0",
128
+ "CTRIA3,1,1,1,2,3",
129
+ "PSHELL,1,1,0.1",
130
+ "MAT1,1,2.1E11,,0.3",
131
+ "ENDDATA",
132
+ "",
133
+ "Cards are comma separated (free field) or 8 character columns (fixed field).",
134
+ "GRID is id, cp, x1, x2, x3.",
135
+ "Element cards are id, property id, then grid ids.",
136
+ "$ starts a comment.",
137
+ "The file must end with ENDDATA.",
138
+ ].join("\n");
139
+ export const format = {
140
+ slug: "nastran-bdf",
141
+ displayName: "Nastran BDF",
142
+ extensions: [".bdf", ".dat", ".nas"],
143
+ description: "Parse Nastran BDF cards, validate grids, elements and ENDDATA.",
144
+ reference,
145
+ parse,
146
+ sample: () => [
147
+ "GRID,1,,0.0,0.0,0.0",
148
+ "GRID,2,,1.0,0.0,0.0",
149
+ "GRID,3,,1.0,1.0,0.0",
150
+ "CTRIA3,1,1,1,2,3",
151
+ "PSHELL,1,1,0.1",
152
+ "MAT1,1,2.1E11,,0.3",
153
+ "ENDDATA",
154
+ ].join("\n") + "\n",
155
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createServer } from "./server.js";
4
+ async function main() {
5
+ const server = createServer();
6
+ const transport = new StdioServerTransport();
7
+ await server.connect(transport);
8
+ }
9
+ main().catch((error) => {
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ process.stderr.write(`${message}\n`);
12
+ process.exit(1);
13
+ });
package/dist/run.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export declare class ToolError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare function errorMessage(error: unknown): string;
5
+ export declare function text(value: string): {
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ };
11
+ export declare function fail(message: string): {
12
+ isError: boolean;
13
+ content: {
14
+ type: "text";
15
+ text: string;
16
+ }[];
17
+ };
package/dist/run.js ADDED
@@ -0,0 +1,15 @@
1
+ export class ToolError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ToolError";
5
+ }
6
+ }
7
+ export function errorMessage(error) {
8
+ return error instanceof Error ? error.message : String(error);
9
+ }
10
+ export function text(value) {
11
+ return { content: [{ type: "text", text: value }] };
12
+ }
13
+ export function fail(message) {
14
+ return { isError: true, content: [{ type: "text", text: message }] };
15
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const VERSION = "1.0.0";
3
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,9 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { format } from "./format.js";
3
+ import { registerFormatTools } from "./core.js";
4
+ export const VERSION = "1.0.0";
5
+ export function createServer() {
6
+ const server = new McpServer({ name: `${format.slug}-mcp`, version: VERSION });
7
+ registerFormatTools(server, format);
8
+ return server;
9
+ }
@@ -0,0 +1,31 @@
1
+ export type Severity = "error" | "warning";
2
+ export interface Issue {
3
+ code: string;
4
+ severity: Severity;
5
+ message: string;
6
+ line?: number;
7
+ }
8
+ export interface ParsedRecord {
9
+ id: string;
10
+ kind: string;
11
+ fields: Record<string, string | number | boolean | null>;
12
+ }
13
+ export interface Parsed {
14
+ format: string;
15
+ records: ParsedRecord[];
16
+ meta: Record<string, string | number | boolean | null>;
17
+ issues: Issue[];
18
+ }
19
+ export interface FormatModule {
20
+ slug: string;
21
+ displayName: string;
22
+ extensions: string[];
23
+ description: string;
24
+ reference: string;
25
+ parse(text: string): Parsed;
26
+ sample(): string;
27
+ regressions?: Array<{
28
+ doc: string;
29
+ expectError: string;
30
+ }>;
31
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@mrfentmen/nastran-bdf-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Nastran MCP: parse BDF cards, validate grids, elements and ENDDATA.",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/nastran-bdf-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mrfentmen/awesome-servers.git"
10
+ },
11
+ "bin": {
12
+ "nastran-bdf-mcp": "./dist/index.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "files": [
16
+ "dist",
17
+ "server.json",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "start": "node dist/index.js",
23
+ "test": "npm run build && node --test tests/tools.test.mjs"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "nastran",
29
+ "fea",
30
+ "bdf",
31
+ "structural",
32
+ "simulation"
33
+ ],
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.0.4",
37
+ "zod": "^3.23.8"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.0.0",
41
+ "typescript": "^5.6.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=20"
45
+ }
46
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.mrfentmen/nastran-bdf-mcp",
4
+ "description": "Nastran MCP: parse BDF cards, validate grids, elements and ENDDATA.",
5
+ "repository": {
6
+ "url": "https://github.com/mrfentmen/awesome-servers",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.1",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@mrfentmen/nastran-bdf-mcp",
14
+ "version": "1.0.1",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }