tina4-nodejs 3.13.145 → 3.13.147

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/CLAUDE.md CHANGED
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
13
13
 
14
14
  The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
15
15
 
16
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.145)
16
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.147)
17
17
 
18
18
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
19
19
 
20
20
  ## What This Project Is
21
21
 
22
- Tina4 for Node.js/TypeScript v3.13.145 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
22
+ Tina4 for Node.js/TypeScript v3.13.147 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
23
23
 
24
24
  The philosophy: zero ceremony, batteries included, file system as source of truth.
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.145",
3
+ "version": "3.13.147",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - native TypeScript conventions and shared Tina4 contracts",
6
6
  "keywords": [
@@ -16940,6 +16940,7 @@ var init_router = __esm({
16940
16940
  path: route.pattern,
16941
16941
  handler: route.filePath ?? (route.handler.name || "(anonymous)"),
16942
16942
  middlewareCount: route.middlewares?.length ?? 0,
16943
+ middleware: (route.middlewares ?? []).map((attached) => typeof attached === "string" ? attached : attached?.name || attached?.constructor?.name || "(anonymous)"),
16943
16944
  cached: route.cached ?? false,
16944
16945
  secure: route.secure ?? false
16945
16946
  });
@@ -27924,7 +27925,8 @@ __export(mcp_exports, {
27924
27925
  mcpTool: () => mcpTool,
27925
27926
  register: () => register,
27926
27927
  registerDevTools: () => registerDevTools,
27927
- schemaFromParams: () => schemaFromParams
27928
+ schemaFromParams: () => schemaFromParams,
27929
+ validateToolArguments: () => validateToolArguments
27928
27930
  });
27929
27931
  import * as fs4 from "node:fs";
27930
27932
  import * as path3 from "node:path";
@@ -27996,6 +27998,27 @@ function schemaFromParams(params) {
27996
27998
  }
27997
27999
  return schema;
27998
28000
  }
28001
+ function validateToolArguments(toolName, schema, args) {
28002
+ const properties = Object.keys(schema.properties ?? {});
28003
+ const takes = properties.length > 0 ? properties.join(", ") : "no arguments";
28004
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
28005
+ return `arguments must be an object (${toolName} takes ${takes})`;
28006
+ }
28007
+ const supplied = args;
28008
+ for (const requiredName of schema.required ?? []) {
28009
+ if (!(requiredName in supplied)) {
28010
+ return `missing required argument '${requiredName}' (${toolName} takes ${takes})`;
28011
+ }
28012
+ }
28013
+ if (!schema.additionalProperties) {
28014
+ for (const suppliedName of Object.keys(supplied)) {
28015
+ if (!properties.includes(suppliedName)) {
28016
+ return `unknown argument '${suppliedName}' (${toolName} takes ${takes})`;
28017
+ }
28018
+ }
28019
+ }
28020
+ return null;
28021
+ }
27999
28022
  function isLocalhost() {
28000
28023
  const hostEnv = process.env.TINA4_HOST_NAME || "localhost:7148";
28001
28024
  const host = hostEnv.split(":")[0];
@@ -28294,7 +28317,12 @@ function registerDevTools(server) {
28294
28317
  if (!/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/.test(table2)) {
28295
28318
  return { error: "Invalid table name" };
28296
28319
  }
28297
- return await db.getColumns?.(table2) ?? [];
28320
+ const columns = await db.getColumns?.(table2) ?? [];
28321
+ if (columns.length === 0) {
28322
+ const tables = await db.getTables?.() ?? [];
28323
+ if (!tables.includes(table2)) return { error: `table not found: ${table2}` };
28324
+ }
28325
+ return columns;
28298
28326
  } catch (e) {
28299
28327
  return { error: e.message };
28300
28328
  }
@@ -28313,7 +28341,8 @@ function registerDevTools(server) {
28313
28341
  return routes.map((r) => ({
28314
28342
  method: r.method || "",
28315
28343
  path: r.pattern || r.path || "",
28316
- auth_required: r.secure ?? false
28344
+ auth_required: r.secure ?? false,
28345
+ middleware: r.middleware ?? []
28317
28346
  }));
28318
28347
  } catch (e) {
28319
28348
  return { error: e.message };
@@ -29459,6 +29488,10 @@ data: ${channel.buffer.shift()}
29459
29488
  throw new Error(`Unknown tool: ${toolName}`);
29460
29489
  }
29461
29490
  const args = params.arguments || {};
29491
+ const invalid = validateToolArguments(toolName, tool.inputSchema, args);
29492
+ if (invalid) {
29493
+ return { content: [{ type: "text", text: JSON.stringify({ error: invalid }, null, 2) }] };
29494
+ }
29462
29495
  const result = await tool.handler(args);
29463
29496
  let content;
29464
29497
  if (typeof result === "string") {
@@ -35497,7 +35530,9 @@ var init_docsParser = __esm({
35497
35530
  // ../core/src/docs.ts
35498
35531
  var docs_exports = {};
35499
35532
  __export(docs_exports, {
35500
- Docs: () => Docs
35533
+ Docs: () => Docs,
35534
+ paramsFromSignature: () => paramsFromSignature,
35535
+ returnFromSignature: () => returnFromSignature
35501
35536
  });
35502
35537
  import * as fs8 from "node:fs";
35503
35538
  import * as path8 from "node:path";
@@ -35724,6 +35759,54 @@ function scoreFqn(entry, tokens) {
35724
35759
  }
35725
35760
  return score;
35726
35761
  }
35762
+ function splitTopLevel(text) {
35763
+ const parts = [];
35764
+ let depth = 0;
35765
+ let current = "";
35766
+ for (const ch of text) {
35767
+ if ("<([{".includes(ch)) depth++;
35768
+ else if (">)]}".includes(ch)) depth--;
35769
+ if (ch === "," && depth === 0) {
35770
+ parts.push(current);
35771
+ current = "";
35772
+ } else current += ch;
35773
+ }
35774
+ if (current.trim()) parts.push(current);
35775
+ return parts;
35776
+ }
35777
+ function closingParen(signature, open) {
35778
+ let depth = 0;
35779
+ for (let i = open; i < signature.length; i++) {
35780
+ if (signature[i] === "(") depth++;
35781
+ else if (signature[i] === ")" && --depth === 0) return i;
35782
+ }
35783
+ return -1;
35784
+ }
35785
+ function paramsFromSignature(signature) {
35786
+ const open = signature.indexOf("(");
35787
+ const close = open < 0 ? -1 : closingParen(signature, open);
35788
+ if (close < 0) return [];
35789
+ return splitTopLevel(signature.slice(open + 1, close)).map((raw) => {
35790
+ const [declaration, ...defaultParts] = raw.split(/=(?!>)/);
35791
+ const defaultText = defaultParts.length > 0 ? defaultParts.join("=").trim() : null;
35792
+ const colon = declaration.indexOf(":");
35793
+ const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
35794
+ const optional = head.endsWith("?");
35795
+ return {
35796
+ name: head.replace(/\?$/, ""),
35797
+ type: colon < 0 ? "" : declaration.slice(colon + 1).trim(),
35798
+ required: !optional && defaultText === null && !head.startsWith("..."),
35799
+ default: defaultText
35800
+ };
35801
+ });
35802
+ }
35803
+ function returnFromSignature(signature) {
35804
+ const open = signature.indexOf("(");
35805
+ const close = open < 0 ? -1 : closingParen(signature, open);
35806
+ if (close < 0) return "";
35807
+ const rest = signature.slice(close + 1).trim();
35808
+ return rest.startsWith(":") ? rest.slice(1).trim() : "";
35809
+ }
35727
35810
  var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
35728
35811
  var init_docs = __esm({
35729
35812
  "../core/src/docs.ts"() {
@@ -36029,8 +36112,8 @@ var init_docs = __esm({
36029
36112
  static: entry.static ?? false,
36030
36113
  source: entry.source,
36031
36114
  version: entry.version,
36032
- params: [],
36033
- return: ""
36115
+ params: paramsFromSignature(entry.signature),
36116
+ return: returnFromSignature(entry.signature)
36034
36117
  };
36035
36118
  }
36036
36119
  /**
@@ -16939,6 +16939,7 @@ var init_router = __esm({
16939
16939
  path: route.pattern,
16940
16940
  handler: route.filePath ?? (route.handler.name || "(anonymous)"),
16941
16941
  middlewareCount: route.middlewares?.length ?? 0,
16942
+ middleware: (route.middlewares ?? []).map((attached) => typeof attached === "string" ? attached : attached?.name || attached?.constructor?.name || "(anonymous)"),
16942
16943
  cached: route.cached ?? false,
16943
16944
  secure: route.secure ?? false
16944
16945
  });
@@ -27880,7 +27881,8 @@ __export(mcp_exports, {
27880
27881
  mcpTool: () => mcpTool,
27881
27882
  register: () => register,
27882
27883
  registerDevTools: () => registerDevTools,
27883
- schemaFromParams: () => schemaFromParams
27884
+ schemaFromParams: () => schemaFromParams,
27885
+ validateToolArguments: () => validateToolArguments
27884
27886
  });
27885
27887
  import * as fs4 from "node:fs";
27886
27888
  import * as path3 from "node:path";
@@ -27952,6 +27954,27 @@ function schemaFromParams(params) {
27952
27954
  }
27953
27955
  return schema;
27954
27956
  }
27957
+ function validateToolArguments(toolName, schema, args) {
27958
+ const properties = Object.keys(schema.properties ?? {});
27959
+ const takes = properties.length > 0 ? properties.join(", ") : "no arguments";
27960
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
27961
+ return `arguments must be an object (${toolName} takes ${takes})`;
27962
+ }
27963
+ const supplied = args;
27964
+ for (const requiredName of schema.required ?? []) {
27965
+ if (!(requiredName in supplied)) {
27966
+ return `missing required argument '${requiredName}' (${toolName} takes ${takes})`;
27967
+ }
27968
+ }
27969
+ if (!schema.additionalProperties) {
27970
+ for (const suppliedName of Object.keys(supplied)) {
27971
+ if (!properties.includes(suppliedName)) {
27972
+ return `unknown argument '${suppliedName}' (${toolName} takes ${takes})`;
27973
+ }
27974
+ }
27975
+ }
27976
+ return null;
27977
+ }
27955
27978
  function isLocalhost() {
27956
27979
  const hostEnv = process.env.TINA4_HOST_NAME || "localhost:7148";
27957
27980
  const host = hostEnv.split(":")[0];
@@ -28250,7 +28273,12 @@ function registerDevTools(server) {
28250
28273
  if (!/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/.test(table2)) {
28251
28274
  return { error: "Invalid table name" };
28252
28275
  }
28253
- return await db.getColumns?.(table2) ?? [];
28276
+ const columns = await db.getColumns?.(table2) ?? [];
28277
+ if (columns.length === 0) {
28278
+ const tables = await db.getTables?.() ?? [];
28279
+ if (!tables.includes(table2)) return { error: `table not found: ${table2}` };
28280
+ }
28281
+ return columns;
28254
28282
  } catch (e) {
28255
28283
  return { error: e.message };
28256
28284
  }
@@ -28269,7 +28297,8 @@ function registerDevTools(server) {
28269
28297
  return routes.map((r) => ({
28270
28298
  method: r.method || "",
28271
28299
  path: r.pattern || r.path || "",
28272
- auth_required: r.secure ?? false
28300
+ auth_required: r.secure ?? false,
28301
+ middleware: r.middleware ?? []
28273
28302
  }));
28274
28303
  } catch (e) {
28275
28304
  return { error: e.message };
@@ -29415,6 +29444,10 @@ data: ${channel.buffer.shift()}
29415
29444
  throw new Error(`Unknown tool: ${toolName}`);
29416
29445
  }
29417
29446
  const args = params.arguments || {};
29447
+ const invalid = validateToolArguments(toolName, tool.inputSchema, args);
29448
+ if (invalid) {
29449
+ return { content: [{ type: "text", text: JSON.stringify({ error: invalid }, null, 2) }] };
29450
+ }
29418
29451
  const result = await tool.handler(args);
29419
29452
  let content;
29420
29453
  if (typeof result === "string") {
@@ -35453,7 +35486,9 @@ var init_docsParser = __esm({
35453
35486
  // src/docs.ts
35454
35487
  var docs_exports = {};
35455
35488
  __export(docs_exports, {
35456
- Docs: () => Docs
35489
+ Docs: () => Docs,
35490
+ paramsFromSignature: () => paramsFromSignature,
35491
+ returnFromSignature: () => returnFromSignature
35457
35492
  });
35458
35493
  import * as fs8 from "node:fs";
35459
35494
  import * as path8 from "node:path";
@@ -35680,6 +35715,54 @@ function scoreFqn(entry, tokens) {
35680
35715
  }
35681
35716
  return score;
35682
35717
  }
35718
+ function splitTopLevel(text) {
35719
+ const parts = [];
35720
+ let depth = 0;
35721
+ let current = "";
35722
+ for (const ch of text) {
35723
+ if ("<([{".includes(ch)) depth++;
35724
+ else if (">)]}".includes(ch)) depth--;
35725
+ if (ch === "," && depth === 0) {
35726
+ parts.push(current);
35727
+ current = "";
35728
+ } else current += ch;
35729
+ }
35730
+ if (current.trim()) parts.push(current);
35731
+ return parts;
35732
+ }
35733
+ function closingParen(signature, open) {
35734
+ let depth = 0;
35735
+ for (let i = open; i < signature.length; i++) {
35736
+ if (signature[i] === "(") depth++;
35737
+ else if (signature[i] === ")" && --depth === 0) return i;
35738
+ }
35739
+ return -1;
35740
+ }
35741
+ function paramsFromSignature(signature) {
35742
+ const open = signature.indexOf("(");
35743
+ const close = open < 0 ? -1 : closingParen(signature, open);
35744
+ if (close < 0) return [];
35745
+ return splitTopLevel(signature.slice(open + 1, close)).map((raw) => {
35746
+ const [declaration, ...defaultParts] = raw.split(/=(?!>)/);
35747
+ const defaultText = defaultParts.length > 0 ? defaultParts.join("=").trim() : null;
35748
+ const colon = declaration.indexOf(":");
35749
+ const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
35750
+ const optional = head.endsWith("?");
35751
+ return {
35752
+ name: head.replace(/\?$/, ""),
35753
+ type: colon < 0 ? "" : declaration.slice(colon + 1).trim(),
35754
+ required: !optional && defaultText === null && !head.startsWith("..."),
35755
+ default: defaultText
35756
+ };
35757
+ });
35758
+ }
35759
+ function returnFromSignature(signature) {
35760
+ const open = signature.indexOf("(");
35761
+ const close = open < 0 ? -1 : closingParen(signature, open);
35762
+ if (close < 0) return "";
35763
+ const rest = signature.slice(close + 1).trim();
35764
+ return rest.startsWith(":") ? rest.slice(1).trim() : "";
35765
+ }
35683
35766
  var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
35684
35767
  var init_docs = __esm({
35685
35768
  "src/docs.ts"() {
@@ -35985,8 +36068,8 @@ var init_docs = __esm({
35985
36068
  static: entry.static ?? false,
35986
36069
  source: entry.source,
35987
36070
  version: entry.version,
35988
- params: [],
35989
- return: ""
36071
+ params: paramsFromSignature(entry.signature),
36072
+ return: returnFromSignature(entry.signature)
35990
36073
  };
35991
36074
  }
35992
36075
  /**
@@ -61,7 +61,7 @@ export interface MethodSpec {
61
61
  static: boolean;
62
62
  source: "framework" | "user" | "vendor";
63
63
  version: string;
64
- params: Array<{ name: string; type: string; default?: string | null }>;
64
+ params: Array<{ name: string; type: string; required: boolean; default: string | null }>;
65
65
  return: string;
66
66
  }
67
67
 
@@ -448,6 +448,59 @@ function scoreFqn(entry: InternalEntry, tokens: string[]): number {
448
448
  */
449
449
  const sharedFrameworkIndex = new Map<string, { entries: Map<string, InternalEntry>; mtime: number }>();
450
450
 
451
+ /** Split `text` on top-level commas, ignoring commas nested in <>, (), [] or {}. */
452
+ function splitTopLevel(text: string): string[] {
453
+ const parts: string[] = [];
454
+ let depth = 0;
455
+ let current = "";
456
+ for (const ch of text) {
457
+ if ("<([{".includes(ch)) depth++;
458
+ else if (">)]}".includes(ch)) depth--;
459
+ if (ch === "," && depth === 0) { parts.push(current); current = ""; } else current += ch;
460
+ }
461
+ if (current.trim()) parts.push(current);
462
+ return parts;
463
+ }
464
+
465
+ /** Index of the `)` closing the parameter list that opens at `open`, or -1. */
466
+ function closingParen(signature: string, open: number): number {
467
+ let depth = 0;
468
+ for (let i = open; i < signature.length; i++) {
469
+ if (signature[i] === "(") depth++;
470
+ else if (signature[i] === ")" && --depth === 0) return i;
471
+ }
472
+ return -1;
473
+ }
474
+
475
+ /** Structured params `[{name, type, required, default}]` from `name(a: T, b?: U = 1) : R`. */
476
+ export function paramsFromSignature(signature: string): MethodSpec["params"] {
477
+ const open = signature.indexOf("(");
478
+ const close = open < 0 ? -1 : closingParen(signature, open);
479
+ if (close < 0) return [];
480
+ return splitTopLevel(signature.slice(open + 1, close)).map((raw) => {
481
+ const [declaration, ...defaultParts] = raw.split(/=(?!>)/);
482
+ const defaultText = defaultParts.length > 0 ? defaultParts.join("=").trim() : null;
483
+ const colon = declaration.indexOf(":");
484
+ const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
485
+ const optional = head.endsWith("?");
486
+ return {
487
+ name: head.replace(/\?$/, ""),
488
+ type: colon < 0 ? "" : declaration.slice(colon + 1).trim(),
489
+ required: !optional && defaultText === null && !head.startsWith("..."),
490
+ default: defaultText,
491
+ };
492
+ });
493
+ }
494
+
495
+ /** Return type text from `name(...) : R` (or `): R`), "" when absent. */
496
+ export function returnFromSignature(signature: string): string {
497
+ const open = signature.indexOf("(");
498
+ const close = open < 0 ? -1 : closingParen(signature, open);
499
+ if (close < 0) return "";
500
+ const rest = signature.slice(close + 1).trim();
501
+ return rest.startsWith(":") ? rest.slice(1).trim() : "";
502
+ }
503
+
451
504
  export class Docs {
452
505
  private projectRoot: string;
453
506
  private frameworkRoots: string[];
@@ -622,8 +675,8 @@ export class Docs {
622
675
  static: entry.static ?? false,
623
676
  source: entry.source,
624
677
  version: entry.version,
625
- params: [],
626
- return: "",
678
+ params: paramsFromSignature(entry.signature),
679
+ return: returnFromSignature(entry.signature),
627
680
  };
628
681
  }
629
682
 
@@ -91,6 +91,7 @@ export interface JsonSchema {
91
91
  type: string;
92
92
  properties: Record<string, { type: string; default?: unknown }>;
93
93
  required?: string[];
94
+ additionalProperties?: boolean;
94
95
  }
95
96
 
96
97
  export interface McpToolParam {
@@ -203,6 +204,36 @@ export function schemaFromParams(params: McpToolParam[]): JsonSchema {
203
204
  return schema;
204
205
  }
205
206
 
207
+ /**
208
+ * Check call arguments against a tool's input schema BEFORE the handler runs.
209
+ * Returns null when valid, else the actionable message
210
+ * `missing required argument '<p>' (<tool> takes <p1>, <p2>)` (or
211
+ * `unknown argument '<k>' (...)`). Byte-identical to tina4-python's
212
+ * `_validate_tool_arguments` (tina4-php#271) so a wrong key never surfaces as a
213
+ * raw TypeError / 500.
214
+ */
215
+ export function validateToolArguments(toolName: string, schema: JsonSchema, args: unknown): string | null {
216
+ const properties = Object.keys(schema.properties ?? {});
217
+ const takes = properties.length > 0 ? properties.join(", ") : "no arguments";
218
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
219
+ return `arguments must be an object (${toolName} takes ${takes})`;
220
+ }
221
+ const supplied = args as Record<string, unknown>;
222
+ for (const requiredName of schema.required ?? []) {
223
+ if (!(requiredName in supplied)) {
224
+ return `missing required argument '${requiredName}' (${toolName} takes ${takes})`;
225
+ }
226
+ }
227
+ if (!schema.additionalProperties) {
228
+ for (const suppliedName of Object.keys(supplied)) {
229
+ if (!properties.includes(suppliedName)) {
230
+ return `unknown argument '${suppliedName}' (${toolName} takes ${takes})`;
231
+ }
232
+ }
233
+ }
234
+ return null;
235
+ }
236
+
206
237
  // ── Localhost detection ──────────────────────────────────────
207
238
 
208
239
  /**
@@ -574,6 +605,10 @@ export class McpServer {
574
605
  }
575
606
 
576
607
  const args = (params.arguments as Record<string, unknown>) || {};
608
+ const invalid = validateToolArguments(toolName, tool.inputSchema, args);
609
+ if (invalid) {
610
+ return { content: [{ type: "text", text: JSON.stringify({ error: invalid }, null, 2) }] };
611
+ }
577
612
  // Tool handlers may be async (the DB tools await the Database wrapper); a
578
613
  // sync handler's plain return value passes through `await` unchanged.
579
614
  const result = await tool.handler(args);
@@ -1139,7 +1174,14 @@ export function registerDevTools(server: McpServer): void {
1139
1174
  if (!/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/.test(table)) {
1140
1175
  return { error: "Invalid table name" };
1141
1176
  }
1142
- return (await db.getColumns?.(table)) ?? [];
1177
+ // Schema metadata (PRAGMA / information_schema via the adapter), never the
1178
+ // first row, so an EMPTY table still reports its columns (tina4-php#271).
1179
+ const columns = (await db.getColumns?.(table)) ?? [];
1180
+ if (columns.length === 0) {
1181
+ const tables = ((await db.getTables?.()) ?? []) as string[];
1182
+ if (!tables.includes(table)) return { error: `table not found: ${table}` };
1183
+ }
1184
+ return columns;
1143
1185
  } catch (e) {
1144
1186
  return { error: (e as Error).message };
1145
1187
  }
@@ -1168,6 +1210,7 @@ export function registerDevTools(server: McpServer): void {
1168
1210
  method: r.method || "",
1169
1211
  path: r.pattern || r.path || "",
1170
1212
  auth_required: r.secure ?? false,
1213
+ middleware: r.middleware ?? [],
1171
1214
  }));
1172
1215
  } catch (e) {
1173
1216
  return { error: (e as Error).message };
@@ -187,6 +187,8 @@ export interface RouteInfo {
187
187
  path: string;
188
188
  handler: string;
189
189
  middlewareCount: number;
190
+ /** Names of the middleware attached to the route (functions / classes / string specs). */
191
+ middleware: string[];
190
192
  cached: boolean;
191
193
  secure: boolean;
192
194
  }
@@ -486,6 +488,8 @@ export class Router {
486
488
  path: route.pattern,
487
489
  handler: route.filePath ?? (route.handler.name || "(anonymous)"),
488
490
  middlewareCount: route.middlewares?.length ?? 0,
491
+ middleware: (route.middlewares ?? []).map((attached: any) =>
492
+ typeof attached === "string" ? attached : (attached?.name || attached?.constructor?.name || "(anonymous)")),
489
493
  cached: route.cached ?? false,
490
494
  secure: route.secure ?? false,
491
495
  });
@@ -4915,6 +4915,7 @@ var init_router = __esm({
4915
4915
  path: route.pattern,
4916
4916
  handler: route.filePath ?? (route.handler.name || "(anonymous)"),
4917
4917
  middlewareCount: route.middlewares?.length ?? 0,
4918
+ middleware: (route.middlewares ?? []).map((attached) => typeof attached === "string" ? attached : attached?.name || attached?.constructor?.name || "(anonymous)"),
4918
4919
  cached: route.cached ?? false,
4919
4920
  secure: route.secure ?? false
4920
4921
  });
@@ -15856,7 +15857,8 @@ __export(mcp_exports, {
15856
15857
  mcpTool: () => mcpTool,
15857
15858
  register: () => register,
15858
15859
  registerDevTools: () => registerDevTools,
15859
- schemaFromParams: () => schemaFromParams
15860
+ schemaFromParams: () => schemaFromParams,
15861
+ validateToolArguments: () => validateToolArguments
15860
15862
  });
15861
15863
  import * as fs4 from "node:fs";
15862
15864
  import * as path3 from "node:path";
@@ -15928,6 +15930,27 @@ function schemaFromParams(params) {
15928
15930
  }
15929
15931
  return schema;
15930
15932
  }
15933
+ function validateToolArguments(toolName, schema, args) {
15934
+ const properties = Object.keys(schema.properties ?? {});
15935
+ const takes = properties.length > 0 ? properties.join(", ") : "no arguments";
15936
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
15937
+ return `arguments must be an object (${toolName} takes ${takes})`;
15938
+ }
15939
+ const supplied = args;
15940
+ for (const requiredName of schema.required ?? []) {
15941
+ if (!(requiredName in supplied)) {
15942
+ return `missing required argument '${requiredName}' (${toolName} takes ${takes})`;
15943
+ }
15944
+ }
15945
+ if (!schema.additionalProperties) {
15946
+ for (const suppliedName of Object.keys(supplied)) {
15947
+ if (!properties.includes(suppliedName)) {
15948
+ return `unknown argument '${suppliedName}' (${toolName} takes ${takes})`;
15949
+ }
15950
+ }
15951
+ }
15952
+ return null;
15953
+ }
15931
15954
  function isLocalhost() {
15932
15955
  const hostEnv = process.env.TINA4_HOST_NAME || "localhost:7148";
15933
15956
  const host = hostEnv.split(":")[0];
@@ -16226,7 +16249,12 @@ function registerDevTools(server) {
16226
16249
  if (!/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/.test(table2)) {
16227
16250
  return { error: "Invalid table name" };
16228
16251
  }
16229
- return await db.getColumns?.(table2) ?? [];
16252
+ const columns = await db.getColumns?.(table2) ?? [];
16253
+ if (columns.length === 0) {
16254
+ const tables = await db.getTables?.() ?? [];
16255
+ if (!tables.includes(table2)) return { error: `table not found: ${table2}` };
16256
+ }
16257
+ return columns;
16230
16258
  } catch (e) {
16231
16259
  return { error: e.message };
16232
16260
  }
@@ -16245,7 +16273,8 @@ function registerDevTools(server) {
16245
16273
  return routes.map((r) => ({
16246
16274
  method: r.method || "",
16247
16275
  path: r.pattern || r.path || "",
16248
- auth_required: r.secure ?? false
16276
+ auth_required: r.secure ?? false,
16277
+ middleware: r.middleware ?? []
16249
16278
  }));
16250
16279
  } catch (e) {
16251
16280
  return { error: e.message };
@@ -17391,6 +17420,10 @@ data: ${channel.buffer.shift()}
17391
17420
  throw new Error(`Unknown tool: ${toolName}`);
17392
17421
  }
17393
17422
  const args = params.arguments || {};
17423
+ const invalid = validateToolArguments(toolName, tool.inputSchema, args);
17424
+ if (invalid) {
17425
+ return { content: [{ type: "text", text: JSON.stringify({ error: invalid }, null, 2) }] };
17426
+ }
17394
17427
  const result = await tool.handler(args);
17395
17428
  let content;
17396
17429
  if (typeof result === "string") {
@@ -23429,7 +23462,9 @@ var init_docsParser = __esm({
23429
23462
  // ../core/src/docs.ts
23430
23463
  var docs_exports = {};
23431
23464
  __export(docs_exports, {
23432
- Docs: () => Docs
23465
+ Docs: () => Docs,
23466
+ paramsFromSignature: () => paramsFromSignature,
23467
+ returnFromSignature: () => returnFromSignature
23433
23468
  });
23434
23469
  import * as fs8 from "node:fs";
23435
23470
  import * as path8 from "node:path";
@@ -23656,6 +23691,54 @@ function scoreFqn(entry, tokens) {
23656
23691
  }
23657
23692
  return score;
23658
23693
  }
23694
+ function splitTopLevel(text) {
23695
+ const parts = [];
23696
+ let depth = 0;
23697
+ let current = "";
23698
+ for (const ch of text) {
23699
+ if ("<([{".includes(ch)) depth++;
23700
+ else if (">)]}".includes(ch)) depth--;
23701
+ if (ch === "," && depth === 0) {
23702
+ parts.push(current);
23703
+ current = "";
23704
+ } else current += ch;
23705
+ }
23706
+ if (current.trim()) parts.push(current);
23707
+ return parts;
23708
+ }
23709
+ function closingParen(signature, open) {
23710
+ let depth = 0;
23711
+ for (let i = open; i < signature.length; i++) {
23712
+ if (signature[i] === "(") depth++;
23713
+ else if (signature[i] === ")" && --depth === 0) return i;
23714
+ }
23715
+ return -1;
23716
+ }
23717
+ function paramsFromSignature(signature) {
23718
+ const open = signature.indexOf("(");
23719
+ const close = open < 0 ? -1 : closingParen(signature, open);
23720
+ if (close < 0) return [];
23721
+ return splitTopLevel(signature.slice(open + 1, close)).map((raw) => {
23722
+ const [declaration, ...defaultParts] = raw.split(/=(?!>)/);
23723
+ const defaultText = defaultParts.length > 0 ? defaultParts.join("=").trim() : null;
23724
+ const colon = declaration.indexOf(":");
23725
+ const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
23726
+ const optional = head.endsWith("?");
23727
+ return {
23728
+ name: head.replace(/\?$/, ""),
23729
+ type: colon < 0 ? "" : declaration.slice(colon + 1).trim(),
23730
+ required: !optional && defaultText === null && !head.startsWith("..."),
23731
+ default: defaultText
23732
+ };
23733
+ });
23734
+ }
23735
+ function returnFromSignature(signature) {
23736
+ const open = signature.indexOf("(");
23737
+ const close = open < 0 ? -1 : closingParen(signature, open);
23738
+ if (close < 0) return "";
23739
+ const rest = signature.slice(close + 1).trim();
23740
+ return rest.startsWith(":") ? rest.slice(1).trim() : "";
23741
+ }
23659
23742
  var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
23660
23743
  var init_docs = __esm({
23661
23744
  "../core/src/docs.ts"() {
@@ -23961,8 +24044,8 @@ var init_docs = __esm({
23961
24044
  static: entry.static ?? false,
23962
24045
  source: entry.source,
23963
24046
  version: entry.version,
23964
- params: [],
23965
- return: ""
24047
+ params: paramsFromSignature(entry.signature),
24048
+ return: returnFromSignature(entry.signature)
23966
24049
  };
23967
24050
  }
23968
24051
  /**
@@ -30,7 +30,8 @@ export interface MethodSpec {
30
30
  params: Array<{
31
31
  name: string;
32
32
  type: string;
33
- default?: string | null;
33
+ required: boolean;
34
+ default: string | null;
34
35
  }>;
35
36
  return: string;
36
37
  }
@@ -69,6 +70,10 @@ export interface DriftHit {
69
70
  line: number;
70
71
  block: string;
71
72
  }
73
+ /** Structured params `[{name, type, required, default}]` from `name(a: T, b?: U = 1) : R`. */
74
+ export declare function paramsFromSignature(signature: string): MethodSpec["params"];
75
+ /** Return type text from `name(...) : R` (or `): R`), "" when absent. */
76
+ export declare function returnFromSignature(signature: string): string;
72
77
  export declare class Docs {
73
78
  private projectRoot;
74
79
  private frameworkRoots;
@@ -30,6 +30,7 @@ export interface JsonSchema {
30
30
  default?: unknown;
31
31
  }>;
32
32
  required?: string[];
33
+ additionalProperties?: boolean;
33
34
  }
34
35
  export interface McpToolParam {
35
36
  name: string;
@@ -57,6 +58,15 @@ export declare function decodeRequest(data: string | Record<string, unknown>): {
57
58
  * Since TypeScript erases types at runtime, we use explicit metadata.
58
59
  */
59
60
  export declare function schemaFromParams(params: McpToolParam[]): JsonSchema;
61
+ /**
62
+ * Check call arguments against a tool's input schema BEFORE the handler runs.
63
+ * Returns null when valid, else the actionable message
64
+ * `missing required argument '<p>' (<tool> takes <p1>, <p2>)` (or
65
+ * `unknown argument '<k>' (...)`). Byte-identical to tina4-python's
66
+ * `_validate_tool_arguments` (tina4-php#271) so a wrong key never surfaces as a
67
+ * raw TypeError / 500.
68
+ */
69
+ export declare function validateToolArguments(toolName: string, schema: JsonSchema, args: unknown): string | null;
60
70
  /**
61
71
  * Informational only — whether the CONFIGURED host looks local.
62
72
  *
@@ -92,6 +92,8 @@ export interface RouteInfo {
92
92
  path: string;
93
93
  handler: string;
94
94
  middlewareCount: number;
95
+ /** Names of the middleware attached to the route (functions / classes / string specs). */
96
+ middleware: string[];
95
97
  cached: boolean;
96
98
  secure: boolean;
97
99
  }