tina4-nodejs 3.13.146 → 3.13.148

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.146)
16
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.148)
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.146 - 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.148 - 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.146",
3
+ "version": "3.13.148",
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
  });
@@ -23877,6 +23878,7 @@ __export(session_exports, {
23877
23878
  isSecureScheme: () => isSecureScheme,
23878
23879
  isValidSessionId: () => isValidSessionId,
23879
23880
  sessionCookieName: () => sessionCookieName,
23881
+ sessionCookieToSend: () => sessionCookieToSend,
23880
23882
  sessionStrictMode: () => sessionStrictMode
23881
23883
  });
23882
23884
  import { createHash as createHash5, randomBytes as randomBytes6 } from "node:crypto";
@@ -23937,6 +23939,15 @@ function buildSessionCookie(sessionId, ttl, cookieName, forwardedProto, socketEn
23937
23939
  parts.push(`Max-Age=${ttl}`);
23938
23940
  return parts.join("; ");
23939
23941
  }
23942
+ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
23943
+ const newSid = sess.sessionId ?? sess.getSessionId?.();
23944
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
23945
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
23946
+ const xfProto = rawReq.headers["x-forwarded-proto"];
23947
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
23948
+ const socketEncrypted = rawReq.socket?.encrypted === true;
23949
+ return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
23950
+ }
23940
23951
  var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
23941
23952
  var init_session = __esm({
23942
23953
  "../core/src/session.ts"() {
@@ -24299,7 +24310,6 @@ var init_session = __esm({
24299
24310
  const now = Math.floor(Date.now() / 1e3);
24300
24311
  this.data = { _created: now, _accessed: now };
24301
24312
  this.dirty = false;
24302
- this.safeWrite(this.sessionId, this.data, this.ttl);
24303
24313
  this.stored = false;
24304
24314
  this.loaded = /* @__PURE__ */ new Map();
24305
24315
  this.cleared = false;
@@ -24450,6 +24460,19 @@ var init_session = __esm({
24450
24460
  const result = this.flash(key);
24451
24461
  return result !== void 0 ? result : defaultValue;
24452
24462
  }
24463
+ /**
24464
+ * Whether this session is only an id: nothing has stored it and it holds no
24465
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
24466
+ * Such a session has no record and needs no cookie, so save() writes nothing
24467
+ * for it and the server sends no cookie for it. It stops being fresh when
24468
+ * something is stored in it. Calls that mark a session changed without leaving
24469
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
24470
+ * empty session) leave it fresh: a record with no data is not a session.
24471
+ */
24472
+ isFresh() {
24473
+ if (this.stored) return false;
24474
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
24475
+ }
24453
24476
  /**
24454
24477
  * Get the current session ID.
24455
24478
  */
@@ -24512,7 +24535,7 @@ var init_session = __esm({
24512
24535
  */
24513
24536
  save() {
24514
24537
  if (!this.sessionId || !this.data) return true;
24515
- if (!this.dirty && !this.stored) return true;
24538
+ if (this.isFresh()) return true;
24516
24539
  let record = this.data;
24517
24540
  if (this.stored) {
24518
24541
  const current = this.safeRead(this.sessionId);
@@ -24699,7 +24722,7 @@ function errorLabel(err) {
24699
24722
  return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
24700
24723
  }
24701
24724
  async function sessionAutoStart(rawReq, rawRes, req2) {
24702
- const { Session: Session2, buildSessionCookie: buildSessionCookie2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24725
+ const { Session: Session2, sessionCookieToSend: sessionCookieToSend2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24703
24726
  const cookieHeader = rawReq.headers.cookie ?? "";
24704
24727
  const cookiePrefix = sessionCookieName2() + "=";
24705
24728
  let existingSid;
@@ -24738,13 +24761,9 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
24738
24761
  } catch {
24739
24762
  }
24740
24763
  }
24741
- const newSid = sess.sessionId ?? sess.getSessionId?.();
24742
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
24743
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
24744
- const xfProto = rawReq.headers["x-forwarded-proto"];
24745
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
24746
- const socketEncrypted = rawReq.socket?.encrypted === true;
24747
- rawRes.appendHeader("Set-Cookie", buildSessionCookie2(newSid, ttl, void 0, forwardedProto, socketEncrypted));
24764
+ const sessionCookie = sessionCookieToSend2(sess, existingSid, rawReq, rawRes);
24765
+ if (sessionCookie !== null) {
24766
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
24748
24767
  }
24749
24768
  return origEnd(...args);
24750
24769
  };
@@ -27924,7 +27943,8 @@ __export(mcp_exports, {
27924
27943
  mcpTool: () => mcpTool,
27925
27944
  register: () => register,
27926
27945
  registerDevTools: () => registerDevTools,
27927
- schemaFromParams: () => schemaFromParams
27946
+ schemaFromParams: () => schemaFromParams,
27947
+ validateToolArguments: () => validateToolArguments
27928
27948
  });
27929
27949
  import * as fs4 from "node:fs";
27930
27950
  import * as path3 from "node:path";
@@ -27996,6 +28016,27 @@ function schemaFromParams(params) {
27996
28016
  }
27997
28017
  return schema;
27998
28018
  }
28019
+ function validateToolArguments(toolName, schema, args) {
28020
+ const properties = Object.keys(schema.properties ?? {});
28021
+ const takes = properties.length > 0 ? properties.join(", ") : "no arguments";
28022
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
28023
+ return `arguments must be an object (${toolName} takes ${takes})`;
28024
+ }
28025
+ const supplied = args;
28026
+ for (const requiredName of schema.required ?? []) {
28027
+ if (!(requiredName in supplied)) {
28028
+ return `missing required argument '${requiredName}' (${toolName} takes ${takes})`;
28029
+ }
28030
+ }
28031
+ if (!schema.additionalProperties) {
28032
+ for (const suppliedName of Object.keys(supplied)) {
28033
+ if (!properties.includes(suppliedName)) {
28034
+ return `unknown argument '${suppliedName}' (${toolName} takes ${takes})`;
28035
+ }
28036
+ }
28037
+ }
28038
+ return null;
28039
+ }
27999
28040
  function isLocalhost() {
28000
28041
  const hostEnv = process.env.TINA4_HOST_NAME || "localhost:7148";
28001
28042
  const host = hostEnv.split(":")[0];
@@ -28294,7 +28335,12 @@ function registerDevTools(server) {
28294
28335
  if (!/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/.test(table2)) {
28295
28336
  return { error: "Invalid table name" };
28296
28337
  }
28297
- return await db.getColumns?.(table2) ?? [];
28338
+ const columns = await db.getColumns?.(table2) ?? [];
28339
+ if (columns.length === 0) {
28340
+ const tables = await db.getTables?.() ?? [];
28341
+ if (!tables.includes(table2)) return { error: `table not found: ${table2}` };
28342
+ }
28343
+ return columns;
28298
28344
  } catch (e) {
28299
28345
  return { error: e.message };
28300
28346
  }
@@ -28313,7 +28359,8 @@ function registerDevTools(server) {
28313
28359
  return routes.map((r) => ({
28314
28360
  method: r.method || "",
28315
28361
  path: r.pattern || r.path || "",
28316
- auth_required: r.secure ?? false
28362
+ auth_required: r.secure ?? false,
28363
+ middleware: r.middleware ?? []
28317
28364
  }));
28318
28365
  } catch (e) {
28319
28366
  return { error: e.message };
@@ -29459,6 +29506,10 @@ data: ${channel.buffer.shift()}
29459
29506
  throw new Error(`Unknown tool: ${toolName}`);
29460
29507
  }
29461
29508
  const args = params.arguments || {};
29509
+ const invalid = validateToolArguments(toolName, tool.inputSchema, args);
29510
+ if (invalid) {
29511
+ return { content: [{ type: "text", text: JSON.stringify({ error: invalid }, null, 2) }] };
29512
+ }
29462
29513
  const result = await tool.handler(args);
29463
29514
  let content;
29464
29515
  if (typeof result === "string") {
@@ -35497,7 +35548,9 @@ var init_docsParser = __esm({
35497
35548
  // ../core/src/docs.ts
35498
35549
  var docs_exports = {};
35499
35550
  __export(docs_exports, {
35500
- Docs: () => Docs
35551
+ Docs: () => Docs,
35552
+ paramsFromSignature: () => paramsFromSignature,
35553
+ returnFromSignature: () => returnFromSignature
35501
35554
  });
35502
35555
  import * as fs8 from "node:fs";
35503
35556
  import * as path8 from "node:path";
@@ -35724,6 +35777,54 @@ function scoreFqn(entry, tokens) {
35724
35777
  }
35725
35778
  return score;
35726
35779
  }
35780
+ function splitTopLevel(text) {
35781
+ const parts = [];
35782
+ let depth = 0;
35783
+ let current = "";
35784
+ for (const ch of text) {
35785
+ if ("<([{".includes(ch)) depth++;
35786
+ else if (">)]}".includes(ch)) depth--;
35787
+ if (ch === "," && depth === 0) {
35788
+ parts.push(current);
35789
+ current = "";
35790
+ } else current += ch;
35791
+ }
35792
+ if (current.trim()) parts.push(current);
35793
+ return parts;
35794
+ }
35795
+ function closingParen(signature, open) {
35796
+ let depth = 0;
35797
+ for (let i = open; i < signature.length; i++) {
35798
+ if (signature[i] === "(") depth++;
35799
+ else if (signature[i] === ")" && --depth === 0) return i;
35800
+ }
35801
+ return -1;
35802
+ }
35803
+ function paramsFromSignature(signature) {
35804
+ const open = signature.indexOf("(");
35805
+ const close = open < 0 ? -1 : closingParen(signature, open);
35806
+ if (close < 0) return [];
35807
+ return splitTopLevel(signature.slice(open + 1, close)).map((raw) => {
35808
+ const [declaration, ...defaultParts] = raw.split(/=(?!>)/);
35809
+ const defaultText = defaultParts.length > 0 ? defaultParts.join("=").trim() : null;
35810
+ const colon = declaration.indexOf(":");
35811
+ const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
35812
+ const optional = head.endsWith("?");
35813
+ return {
35814
+ name: head.replace(/\?$/, ""),
35815
+ type: colon < 0 ? "" : declaration.slice(colon + 1).trim(),
35816
+ required: !optional && defaultText === null && !head.startsWith("..."),
35817
+ default: defaultText
35818
+ };
35819
+ });
35820
+ }
35821
+ function returnFromSignature(signature) {
35822
+ const open = signature.indexOf("(");
35823
+ const close = open < 0 ? -1 : closingParen(signature, open);
35824
+ if (close < 0) return "";
35825
+ const rest = signature.slice(close + 1).trim();
35826
+ return rest.startsWith(":") ? rest.slice(1).trim() : "";
35827
+ }
35727
35828
  var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
35728
35829
  var init_docs = __esm({
35729
35830
  "../core/src/docs.ts"() {
@@ -36029,8 +36130,8 @@ var init_docs = __esm({
36029
36130
  static: entry.static ?? false,
36030
36131
  source: entry.source,
36031
36132
  version: entry.version,
36032
- params: [],
36033
- return: ""
36133
+ params: paramsFromSignature(entry.signature),
36134
+ return: returnFromSignature(entry.signature)
36034
36135
  };
36035
36136
  }
36036
36137
  /**
@@ -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
  });
@@ -23856,6 +23857,7 @@ __export(session_exports, {
23856
23857
  isSecureScheme: () => isSecureScheme,
23857
23858
  isValidSessionId: () => isValidSessionId,
23858
23859
  sessionCookieName: () => sessionCookieName,
23860
+ sessionCookieToSend: () => sessionCookieToSend,
23859
23861
  sessionStrictMode: () => sessionStrictMode
23860
23862
  });
23861
23863
  import { createHash as createHash5, randomBytes as randomBytes6 } from "node:crypto";
@@ -23916,6 +23918,15 @@ function buildSessionCookie(sessionId, ttl, cookieName, forwardedProto, socketEn
23916
23918
  parts.push(`Max-Age=${ttl}`);
23917
23919
  return parts.join("; ");
23918
23920
  }
23921
+ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
23922
+ const newSid = sess.sessionId ?? sess.getSessionId?.();
23923
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
23924
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
23925
+ const xfProto = rawReq.headers["x-forwarded-proto"];
23926
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
23927
+ const socketEncrypted = rawReq.socket?.encrypted === true;
23928
+ return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
23929
+ }
23919
23930
  var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
23920
23931
  var init_session = __esm({
23921
23932
  "src/session.ts"() {
@@ -24278,7 +24289,6 @@ var init_session = __esm({
24278
24289
  const now = Math.floor(Date.now() / 1e3);
24279
24290
  this.data = { _created: now, _accessed: now };
24280
24291
  this.dirty = false;
24281
- this.safeWrite(this.sessionId, this.data, this.ttl);
24282
24292
  this.stored = false;
24283
24293
  this.loaded = /* @__PURE__ */ new Map();
24284
24294
  this.cleared = false;
@@ -24429,6 +24439,19 @@ var init_session = __esm({
24429
24439
  const result = this.flash(key);
24430
24440
  return result !== void 0 ? result : defaultValue;
24431
24441
  }
24442
+ /**
24443
+ * Whether this session is only an id: nothing has stored it and it holds no
24444
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
24445
+ * Such a session has no record and needs no cookie, so save() writes nothing
24446
+ * for it and the server sends no cookie for it. It stops being fresh when
24447
+ * something is stored in it. Calls that mark a session changed without leaving
24448
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
24449
+ * empty session) leave it fresh: a record with no data is not a session.
24450
+ */
24451
+ isFresh() {
24452
+ if (this.stored) return false;
24453
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
24454
+ }
24432
24455
  /**
24433
24456
  * Get the current session ID.
24434
24457
  */
@@ -24491,7 +24514,7 @@ var init_session = __esm({
24491
24514
  */
24492
24515
  save() {
24493
24516
  if (!this.sessionId || !this.data) return true;
24494
- if (!this.dirty && !this.stored) return true;
24517
+ if (this.isFresh()) return true;
24495
24518
  let record = this.data;
24496
24519
  if (this.stored) {
24497
24520
  const current = this.safeRead(this.sessionId);
@@ -24678,7 +24701,7 @@ function errorLabel(err) {
24678
24701
  return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
24679
24702
  }
24680
24703
  async function sessionAutoStart(rawReq, rawRes, req2) {
24681
- const { Session: Session2, buildSessionCookie: buildSessionCookie2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24704
+ const { Session: Session2, sessionCookieToSend: sessionCookieToSend2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24682
24705
  const cookieHeader = rawReq.headers.cookie ?? "";
24683
24706
  const cookiePrefix = sessionCookieName2() + "=";
24684
24707
  let existingSid;
@@ -24717,13 +24740,9 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
24717
24740
  } catch {
24718
24741
  }
24719
24742
  }
24720
- const newSid = sess.sessionId ?? sess.getSessionId?.();
24721
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
24722
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
24723
- const xfProto = rawReq.headers["x-forwarded-proto"];
24724
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
24725
- const socketEncrypted = rawReq.socket?.encrypted === true;
24726
- rawRes.appendHeader("Set-Cookie", buildSessionCookie2(newSid, ttl, void 0, forwardedProto, socketEncrypted));
24743
+ const sessionCookie = sessionCookieToSend2(sess, existingSid, rawReq, rawRes);
24744
+ if (sessionCookie !== null) {
24745
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
24727
24746
  }
24728
24747
  return origEnd(...args);
24729
24748
  };
@@ -27880,7 +27899,8 @@ __export(mcp_exports, {
27880
27899
  mcpTool: () => mcpTool,
27881
27900
  register: () => register,
27882
27901
  registerDevTools: () => registerDevTools,
27883
- schemaFromParams: () => schemaFromParams
27902
+ schemaFromParams: () => schemaFromParams,
27903
+ validateToolArguments: () => validateToolArguments
27884
27904
  });
27885
27905
  import * as fs4 from "node:fs";
27886
27906
  import * as path3 from "node:path";
@@ -27952,6 +27972,27 @@ function schemaFromParams(params) {
27952
27972
  }
27953
27973
  return schema;
27954
27974
  }
27975
+ function validateToolArguments(toolName, schema, args) {
27976
+ const properties = Object.keys(schema.properties ?? {});
27977
+ const takes = properties.length > 0 ? properties.join(", ") : "no arguments";
27978
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
27979
+ return `arguments must be an object (${toolName} takes ${takes})`;
27980
+ }
27981
+ const supplied = args;
27982
+ for (const requiredName of schema.required ?? []) {
27983
+ if (!(requiredName in supplied)) {
27984
+ return `missing required argument '${requiredName}' (${toolName} takes ${takes})`;
27985
+ }
27986
+ }
27987
+ if (!schema.additionalProperties) {
27988
+ for (const suppliedName of Object.keys(supplied)) {
27989
+ if (!properties.includes(suppliedName)) {
27990
+ return `unknown argument '${suppliedName}' (${toolName} takes ${takes})`;
27991
+ }
27992
+ }
27993
+ }
27994
+ return null;
27995
+ }
27955
27996
  function isLocalhost() {
27956
27997
  const hostEnv = process.env.TINA4_HOST_NAME || "localhost:7148";
27957
27998
  const host = hostEnv.split(":")[0];
@@ -28250,7 +28291,12 @@ function registerDevTools(server) {
28250
28291
  if (!/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/.test(table2)) {
28251
28292
  return { error: "Invalid table name" };
28252
28293
  }
28253
- return await db.getColumns?.(table2) ?? [];
28294
+ const columns = await db.getColumns?.(table2) ?? [];
28295
+ if (columns.length === 0) {
28296
+ const tables = await db.getTables?.() ?? [];
28297
+ if (!tables.includes(table2)) return { error: `table not found: ${table2}` };
28298
+ }
28299
+ return columns;
28254
28300
  } catch (e) {
28255
28301
  return { error: e.message };
28256
28302
  }
@@ -28269,7 +28315,8 @@ function registerDevTools(server) {
28269
28315
  return routes.map((r) => ({
28270
28316
  method: r.method || "",
28271
28317
  path: r.pattern || r.path || "",
28272
- auth_required: r.secure ?? false
28318
+ auth_required: r.secure ?? false,
28319
+ middleware: r.middleware ?? []
28273
28320
  }));
28274
28321
  } catch (e) {
28275
28322
  return { error: e.message };
@@ -29415,6 +29462,10 @@ data: ${channel.buffer.shift()}
29415
29462
  throw new Error(`Unknown tool: ${toolName}`);
29416
29463
  }
29417
29464
  const args = params.arguments || {};
29465
+ const invalid = validateToolArguments(toolName, tool.inputSchema, args);
29466
+ if (invalid) {
29467
+ return { content: [{ type: "text", text: JSON.stringify({ error: invalid }, null, 2) }] };
29468
+ }
29418
29469
  const result = await tool.handler(args);
29419
29470
  let content;
29420
29471
  if (typeof result === "string") {
@@ -35453,7 +35504,9 @@ var init_docsParser = __esm({
35453
35504
  // src/docs.ts
35454
35505
  var docs_exports = {};
35455
35506
  __export(docs_exports, {
35456
- Docs: () => Docs
35507
+ Docs: () => Docs,
35508
+ paramsFromSignature: () => paramsFromSignature,
35509
+ returnFromSignature: () => returnFromSignature
35457
35510
  });
35458
35511
  import * as fs8 from "node:fs";
35459
35512
  import * as path8 from "node:path";
@@ -35680,6 +35733,54 @@ function scoreFqn(entry, tokens) {
35680
35733
  }
35681
35734
  return score;
35682
35735
  }
35736
+ function splitTopLevel(text) {
35737
+ const parts = [];
35738
+ let depth = 0;
35739
+ let current = "";
35740
+ for (const ch of text) {
35741
+ if ("<([{".includes(ch)) depth++;
35742
+ else if (">)]}".includes(ch)) depth--;
35743
+ if (ch === "," && depth === 0) {
35744
+ parts.push(current);
35745
+ current = "";
35746
+ } else current += ch;
35747
+ }
35748
+ if (current.trim()) parts.push(current);
35749
+ return parts;
35750
+ }
35751
+ function closingParen(signature, open) {
35752
+ let depth = 0;
35753
+ for (let i = open; i < signature.length; i++) {
35754
+ if (signature[i] === "(") depth++;
35755
+ else if (signature[i] === ")" && --depth === 0) return i;
35756
+ }
35757
+ return -1;
35758
+ }
35759
+ function paramsFromSignature(signature) {
35760
+ const open = signature.indexOf("(");
35761
+ const close = open < 0 ? -1 : closingParen(signature, open);
35762
+ if (close < 0) return [];
35763
+ return splitTopLevel(signature.slice(open + 1, close)).map((raw) => {
35764
+ const [declaration, ...defaultParts] = raw.split(/=(?!>)/);
35765
+ const defaultText = defaultParts.length > 0 ? defaultParts.join("=").trim() : null;
35766
+ const colon = declaration.indexOf(":");
35767
+ const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
35768
+ const optional = head.endsWith("?");
35769
+ return {
35770
+ name: head.replace(/\?$/, ""),
35771
+ type: colon < 0 ? "" : declaration.slice(colon + 1).trim(),
35772
+ required: !optional && defaultText === null && !head.startsWith("..."),
35773
+ default: defaultText
35774
+ };
35775
+ });
35776
+ }
35777
+ function returnFromSignature(signature) {
35778
+ const open = signature.indexOf("(");
35779
+ const close = open < 0 ? -1 : closingParen(signature, open);
35780
+ if (close < 0) return "";
35781
+ const rest = signature.slice(close + 1).trim();
35782
+ return rest.startsWith(":") ? rest.slice(1).trim() : "";
35783
+ }
35683
35784
  var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
35684
35785
  var init_docs = __esm({
35685
35786
  "src/docs.ts"() {
@@ -35985,8 +36086,8 @@ var init_docs = __esm({
35985
36086
  static: entry.static ?? false,
35986
36087
  source: entry.source,
35987
36088
  version: entry.version,
35988
- params: [],
35989
- return: ""
36089
+ params: paramsFromSignature(entry.signature),
36090
+ return: returnFromSignature(entry.signature)
35990
36091
  };
35991
36092
  }
35992
36093
  /**
@@ -367,7 +367,7 @@ export async function sessionAutoStart(
367
367
  rawRes: ServerResponse,
368
368
  req: Tina4Request,
369
369
  ): Promise<void> {
370
- const { Session, buildSessionCookie, sessionCookieName, sessionStrictMode } =
370
+ const { Session, sessionCookieToSend, sessionCookieName, sessionStrictMode } =
371
371
  await import("./session.js");
372
372
  const cookieHeader = rawReq.headers.cookie ?? "";
373
373
 
@@ -433,16 +433,8 @@ export async function sessionAutoStart(
433
433
  try { sess.gc(); } catch { /* GC failure is non-critical */ }
434
434
  }
435
435
 
436
- const newSid = (sess as any).sessionId ?? (sess as any).getSessionId?.();
437
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
438
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
439
- // Thread the client's real scheme in so an HTTPS deploy behind a
440
- // TLS-terminating proxy ships the session cookie with `Secure`
441
- // (nodejs#34). `x-forwarded-proto` is the same header request.ts trusts
442
- // for URL construction; native socket TLS is the fallback.
443
- const xfProto = rawReq.headers["x-forwarded-proto"];
444
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
445
- const socketEncrypted = (rawReq.socket as { encrypted?: boolean })?.encrypted === true;
436
+ const sessionCookie = sessionCookieToSend(sess, existingSid, rawReq, rawRes);
437
+ if (sessionCookie !== null) {
446
438
  // appendHeader, not setHeader (feature 131 fix, found while proving
447
439
  // TC-DEC-02): setHeader REPLACES any existing Set-Cookie value wholesale,
448
440
  // so a route that had already called response.cookie() of its own — on
@@ -452,7 +444,7 @@ export async function sessionAutoStart(
452
444
  // appendHeader adds to whatever is already there (promoting a scalar to
453
445
  // an array, extending an existing array) and behaves exactly like
454
446
  // setHeader when nothing is set yet, so the common case is unchanged.
455
- rawRes.appendHeader("Set-Cookie", buildSessionCookie(newSid, ttl, undefined, forwardedProto, socketEncrypted));
447
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
456
448
  }
457
449
  return origEnd(...args);
458
450
  } as typeof rawRes.end;
@@ -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
  });
@@ -610,12 +610,14 @@ export class Session {
610
610
  }
611
611
  }
612
612
 
613
- // Generate new session
613
+ // Generate new session. Nothing is written for it yet: a session nobody
614
+ // writes to has nothing to keep, and writing it here stored one record for
615
+ // every request that arrived without a cookie (static files, 404s and
616
+ // /health included). Its first save() after a change writes it whole.
614
617
  this.sessionId = randomBytes(16).toString("hex");
615
618
  const now = Math.floor(Date.now() / 1000);
616
619
  this.data = { _created: now, _accessed: now };
617
620
  this.dirty = false;
618
- this.safeWrite(this.sessionId, this.data, this.ttl);
619
621
  this.stored = false;
620
622
  this.loaded = new Map();
621
623
  this.cleared = false;
@@ -785,6 +787,20 @@ export class Session {
785
787
  return result !== undefined ? result : defaultValue;
786
788
  }
787
789
 
790
+ /**
791
+ * Whether this session is only an id: nothing has stored it and it holds no
792
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
793
+ * Such a session has no record and needs no cookie, so save() writes nothing
794
+ * for it and the server sends no cookie for it. It stops being fresh when
795
+ * something is stored in it. Calls that mark a session changed without leaving
796
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
797
+ * empty session) leave it fresh: a record with no data is not a session.
798
+ */
799
+ isFresh(): boolean {
800
+ if (this.stored) return false;
801
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
802
+ }
803
+
788
804
  /**
789
805
  * Get the current session ID.
790
806
  */
@@ -850,7 +866,7 @@ export class Session {
850
866
  */
851
867
  save(): boolean {
852
868
  if (!this.sessionId || !this.data) return true;
853
- if (!this.dirty && !this.stored) return true;
869
+ if (this.isFresh()) return true;
854
870
  let record: SessionData = this.data;
855
871
  if (this.stored) {
856
872
  const current = this.safeRead(this.sessionId);
@@ -1018,3 +1034,29 @@ export function buildSessionCookie(
1018
1034
  parts.push(`Max-Age=${ttl}`);
1019
1035
  return parts.join("; ");
1020
1036
  }
1037
+
1038
+ /**
1039
+ * The Set-Cookie value that hands the client this request's session, or null
1040
+ * when none is owed: the client already holds the id, headers have gone out, or
1041
+ * the session is fresh. A fresh session (only an id: minted for this request
1042
+ * and holding no data) was not stored, so a cookie for it would name nothing:
1043
+ * the next request could not resume it and would be handed another one.
1044
+ */
1045
+ export function sessionCookieToSend(
1046
+ sess: { isFresh(): boolean },
1047
+ existingSid: string | undefined,
1048
+ rawReq: { headers: Record<string, string | string[] | undefined>; socket?: unknown },
1049
+ rawRes: { headersSent: boolean },
1050
+ ): string | null {
1051
+ const newSid = (sess as any).sessionId ?? (sess as any).getSessionId?.();
1052
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
1053
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
1054
+ // Thread the client's real scheme in so an HTTPS deploy behind a
1055
+ // TLS-terminating proxy ships the session cookie with `Secure`
1056
+ // (nodejs#34). `x-forwarded-proto` is the same header request.ts trusts
1057
+ // for URL construction; native socket TLS is the fallback.
1058
+ const xfProto = rawReq.headers["x-forwarded-proto"];
1059
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
1060
+ const socketEncrypted = (rawReq.socket as { encrypted?: boolean } | undefined)?.encrypted === true;
1061
+ return buildSessionCookie(newSid, ttl, undefined, forwardedProto, socketEncrypted);
1062
+ }
@@ -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
  });
@@ -11832,6 +11833,7 @@ __export(session_exports, {
11832
11833
  isSecureScheme: () => isSecureScheme,
11833
11834
  isValidSessionId: () => isValidSessionId,
11834
11835
  sessionCookieName: () => sessionCookieName,
11836
+ sessionCookieToSend: () => sessionCookieToSend,
11835
11837
  sessionStrictMode: () => sessionStrictMode
11836
11838
  });
11837
11839
  import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
@@ -11892,6 +11894,15 @@ function buildSessionCookie(sessionId, ttl, cookieName, forwardedProto, socketEn
11892
11894
  parts.push(`Max-Age=${ttl}`);
11893
11895
  return parts.join("; ");
11894
11896
  }
11897
+ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
11898
+ const newSid = sess.sessionId ?? sess.getSessionId?.();
11899
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
11900
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
11901
+ const xfProto = rawReq.headers["x-forwarded-proto"];
11902
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
11903
+ const socketEncrypted = rawReq.socket?.encrypted === true;
11904
+ return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
11905
+ }
11895
11906
  var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
11896
11907
  var init_session = __esm({
11897
11908
  "../core/src/session.ts"() {
@@ -12254,7 +12265,6 @@ var init_session = __esm({
12254
12265
  const now = Math.floor(Date.now() / 1e3);
12255
12266
  this.data = { _created: now, _accessed: now };
12256
12267
  this.dirty = false;
12257
- this.safeWrite(this.sessionId, this.data, this.ttl);
12258
12268
  this.stored = false;
12259
12269
  this.loaded = /* @__PURE__ */ new Map();
12260
12270
  this.cleared = false;
@@ -12405,6 +12415,19 @@ var init_session = __esm({
12405
12415
  const result = this.flash(key);
12406
12416
  return result !== void 0 ? result : defaultValue;
12407
12417
  }
12418
+ /**
12419
+ * Whether this session is only an id: nothing has stored it and it holds no
12420
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
12421
+ * Such a session has no record and needs no cookie, so save() writes nothing
12422
+ * for it and the server sends no cookie for it. It stops being fresh when
12423
+ * something is stored in it. Calls that mark a session changed without leaving
12424
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
12425
+ * empty session) leave it fresh: a record with no data is not a session.
12426
+ */
12427
+ isFresh() {
12428
+ if (this.stored) return false;
12429
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
12430
+ }
12408
12431
  /**
12409
12432
  * Get the current session ID.
12410
12433
  */
@@ -12467,7 +12490,7 @@ var init_session = __esm({
12467
12490
  */
12468
12491
  save() {
12469
12492
  if (!this.sessionId || !this.data) return true;
12470
- if (!this.dirty && !this.stored) return true;
12493
+ if (this.isFresh()) return true;
12471
12494
  let record = this.data;
12472
12495
  if (this.stored) {
12473
12496
  const current = this.safeRead(this.sessionId);
@@ -12654,7 +12677,7 @@ function errorLabel(err) {
12654
12677
  return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
12655
12678
  }
12656
12679
  async function sessionAutoStart(rawReq, rawRes, req2) {
12657
- const { Session: Session2, buildSessionCookie: buildSessionCookie2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
12680
+ const { Session: Session2, sessionCookieToSend: sessionCookieToSend2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
12658
12681
  const cookieHeader = rawReq.headers.cookie ?? "";
12659
12682
  const cookiePrefix = sessionCookieName2() + "=";
12660
12683
  let existingSid;
@@ -12693,13 +12716,9 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
12693
12716
  } catch {
12694
12717
  }
12695
12718
  }
12696
- const newSid = sess.sessionId ?? sess.getSessionId?.();
12697
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
12698
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
12699
- const xfProto = rawReq.headers["x-forwarded-proto"];
12700
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
12701
- const socketEncrypted = rawReq.socket?.encrypted === true;
12702
- rawRes.appendHeader("Set-Cookie", buildSessionCookie2(newSid, ttl, void 0, forwardedProto, socketEncrypted));
12719
+ const sessionCookie = sessionCookieToSend2(sess, existingSid, rawReq, rawRes);
12720
+ if (sessionCookie !== null) {
12721
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
12703
12722
  }
12704
12723
  return origEnd(...args);
12705
12724
  };
@@ -15856,7 +15875,8 @@ __export(mcp_exports, {
15856
15875
  mcpTool: () => mcpTool,
15857
15876
  register: () => register,
15858
15877
  registerDevTools: () => registerDevTools,
15859
- schemaFromParams: () => schemaFromParams
15878
+ schemaFromParams: () => schemaFromParams,
15879
+ validateToolArguments: () => validateToolArguments
15860
15880
  });
15861
15881
  import * as fs4 from "node:fs";
15862
15882
  import * as path3 from "node:path";
@@ -15928,6 +15948,27 @@ function schemaFromParams(params) {
15928
15948
  }
15929
15949
  return schema;
15930
15950
  }
15951
+ function validateToolArguments(toolName, schema, args) {
15952
+ const properties = Object.keys(schema.properties ?? {});
15953
+ const takes = properties.length > 0 ? properties.join(", ") : "no arguments";
15954
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
15955
+ return `arguments must be an object (${toolName} takes ${takes})`;
15956
+ }
15957
+ const supplied = args;
15958
+ for (const requiredName of schema.required ?? []) {
15959
+ if (!(requiredName in supplied)) {
15960
+ return `missing required argument '${requiredName}' (${toolName} takes ${takes})`;
15961
+ }
15962
+ }
15963
+ if (!schema.additionalProperties) {
15964
+ for (const suppliedName of Object.keys(supplied)) {
15965
+ if (!properties.includes(suppliedName)) {
15966
+ return `unknown argument '${suppliedName}' (${toolName} takes ${takes})`;
15967
+ }
15968
+ }
15969
+ }
15970
+ return null;
15971
+ }
15931
15972
  function isLocalhost() {
15932
15973
  const hostEnv = process.env.TINA4_HOST_NAME || "localhost:7148";
15933
15974
  const host = hostEnv.split(":")[0];
@@ -16226,7 +16267,12 @@ function registerDevTools(server) {
16226
16267
  if (!/^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?$/.test(table2)) {
16227
16268
  return { error: "Invalid table name" };
16228
16269
  }
16229
- return await db.getColumns?.(table2) ?? [];
16270
+ const columns = await db.getColumns?.(table2) ?? [];
16271
+ if (columns.length === 0) {
16272
+ const tables = await db.getTables?.() ?? [];
16273
+ if (!tables.includes(table2)) return { error: `table not found: ${table2}` };
16274
+ }
16275
+ return columns;
16230
16276
  } catch (e) {
16231
16277
  return { error: e.message };
16232
16278
  }
@@ -16245,7 +16291,8 @@ function registerDevTools(server) {
16245
16291
  return routes.map((r) => ({
16246
16292
  method: r.method || "",
16247
16293
  path: r.pattern || r.path || "",
16248
- auth_required: r.secure ?? false
16294
+ auth_required: r.secure ?? false,
16295
+ middleware: r.middleware ?? []
16249
16296
  }));
16250
16297
  } catch (e) {
16251
16298
  return { error: e.message };
@@ -17391,6 +17438,10 @@ data: ${channel.buffer.shift()}
17391
17438
  throw new Error(`Unknown tool: ${toolName}`);
17392
17439
  }
17393
17440
  const args = params.arguments || {};
17441
+ const invalid = validateToolArguments(toolName, tool.inputSchema, args);
17442
+ if (invalid) {
17443
+ return { content: [{ type: "text", text: JSON.stringify({ error: invalid }, null, 2) }] };
17444
+ }
17394
17445
  const result = await tool.handler(args);
17395
17446
  let content;
17396
17447
  if (typeof result === "string") {
@@ -23429,7 +23480,9 @@ var init_docsParser = __esm({
23429
23480
  // ../core/src/docs.ts
23430
23481
  var docs_exports = {};
23431
23482
  __export(docs_exports, {
23432
- Docs: () => Docs
23483
+ Docs: () => Docs,
23484
+ paramsFromSignature: () => paramsFromSignature,
23485
+ returnFromSignature: () => returnFromSignature
23433
23486
  });
23434
23487
  import * as fs8 from "node:fs";
23435
23488
  import * as path8 from "node:path";
@@ -23656,6 +23709,54 @@ function scoreFqn(entry, tokens) {
23656
23709
  }
23657
23710
  return score;
23658
23711
  }
23712
+ function splitTopLevel(text) {
23713
+ const parts = [];
23714
+ let depth = 0;
23715
+ let current = "";
23716
+ for (const ch of text) {
23717
+ if ("<([{".includes(ch)) depth++;
23718
+ else if (">)]}".includes(ch)) depth--;
23719
+ if (ch === "," && depth === 0) {
23720
+ parts.push(current);
23721
+ current = "";
23722
+ } else current += ch;
23723
+ }
23724
+ if (current.trim()) parts.push(current);
23725
+ return parts;
23726
+ }
23727
+ function closingParen(signature, open) {
23728
+ let depth = 0;
23729
+ for (let i = open; i < signature.length; i++) {
23730
+ if (signature[i] === "(") depth++;
23731
+ else if (signature[i] === ")" && --depth === 0) return i;
23732
+ }
23733
+ return -1;
23734
+ }
23735
+ function paramsFromSignature(signature) {
23736
+ const open = signature.indexOf("(");
23737
+ const close = open < 0 ? -1 : closingParen(signature, open);
23738
+ if (close < 0) return [];
23739
+ return splitTopLevel(signature.slice(open + 1, close)).map((raw) => {
23740
+ const [declaration, ...defaultParts] = raw.split(/=(?!>)/);
23741
+ const defaultText = defaultParts.length > 0 ? defaultParts.join("=").trim() : null;
23742
+ const colon = declaration.indexOf(":");
23743
+ const head = (colon < 0 ? declaration : declaration.slice(0, colon)).trim();
23744
+ const optional = head.endsWith("?");
23745
+ return {
23746
+ name: head.replace(/\?$/, ""),
23747
+ type: colon < 0 ? "" : declaration.slice(colon + 1).trim(),
23748
+ required: !optional && defaultText === null && !head.startsWith("..."),
23749
+ default: defaultText
23750
+ };
23751
+ });
23752
+ }
23753
+ function returnFromSignature(signature) {
23754
+ const open = signature.indexOf("(");
23755
+ const close = open < 0 ? -1 : closingParen(signature, open);
23756
+ if (close < 0) return "";
23757
+ const rest = signature.slice(close + 1).trim();
23758
+ return rest.startsWith(":") ? rest.slice(1).trim() : "";
23759
+ }
23659
23760
  var STDLIB_ALLOWLIST, _DOCS_CACHE, __FILENAME, __DIRNAME, USER_DIRS, CAMEL_SPLIT, FENCE_RE, CALL_RE, sharedFrameworkIndex, Docs;
23660
23761
  var init_docs = __esm({
23661
23762
  "../core/src/docs.ts"() {
@@ -23961,8 +24062,8 @@ var init_docs = __esm({
23961
24062
  static: entry.static ?? false,
23962
24063
  source: entry.source,
23963
24064
  version: entry.version,
23964
- params: [],
23965
- return: ""
24065
+ params: paramsFromSignature(entry.signature),
24066
+ return: returnFromSignature(entry.signature)
23966
24067
  };
23967
24068
  }
23968
24069
  /**
@@ -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
  }
@@ -258,6 +258,16 @@ export declare class Session {
258
258
  * Get flash data by key (alias for flash(key) without value).
259
259
  */
260
260
  getFlash(key: string, defaultValue?: unknown): unknown;
261
+ /**
262
+ * Whether this session is only an id: nothing has stored it and it holds no
263
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
264
+ * Such a session has no record and needs no cookie, so save() writes nothing
265
+ * for it and the server sends no cookie for it. It stops being fresh when
266
+ * something is stored in it. Calls that mark a session changed without leaving
267
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
268
+ * empty session) leave it fresh: a record with no data is not a session.
269
+ */
270
+ isFresh(): boolean;
261
271
  /**
262
272
  * Get the current session ID.
263
273
  */
@@ -388,4 +398,19 @@ export declare function sessionStrictMode(): boolean;
388
398
  * http://localhost dev cookies undeliverable.
389
399
  */
390
400
  export declare function buildSessionCookie(sessionId: string | null, ttl: number, cookieName?: string, forwardedProto?: string, socketEncrypted?: boolean): string;
401
+ /**
402
+ * The Set-Cookie value that hands the client this request's session, or null
403
+ * when none is owed: the client already holds the id, headers have gone out, or
404
+ * the session is fresh. A fresh session (only an id: minted for this request
405
+ * and holding no data) was not stored, so a cookie for it would name nothing:
406
+ * the next request could not resume it and would be handed another one.
407
+ */
408
+ export declare function sessionCookieToSend(sess: {
409
+ isFresh(): boolean;
410
+ }, existingSid: string | undefined, rawReq: {
411
+ headers: Record<string, string | string[] | undefined>;
412
+ socket?: unknown;
413
+ }, rawRes: {
414
+ headersSent: boolean;
415
+ }): string | null;
391
416
  export {};