tina4-nodejs 3.13.146 → 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 +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +89 -6
- package/packages/core/dist/index.js +89 -6
- package/packages/core/src/docs.ts +56 -3
- package/packages/core/src/mcp.ts +44 -1
- package/packages/core/src/router.ts +4 -0
- package/packages/orm/dist/index.js +89 -6
- package/types/core/src/docs.d.ts +6 -1
- package/types/core/src/mcp.d.ts +10 -0
- package/types/core/src/router.d.ts +2 -0
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.
|
|
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.
|
|
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
package/packages/cli/dist/bin.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/types/core/src/docs.d.ts
CHANGED
|
@@ -30,7 +30,8 @@ export interface MethodSpec {
|
|
|
30
30
|
params: Array<{
|
|
31
31
|
name: string;
|
|
32
32
|
type: string;
|
|
33
|
-
|
|
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;
|
package/types/core/src/mcp.d.ts
CHANGED
|
@@ -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
|
*
|