tina4-nodejs 3.13.34 → 3.13.35
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/core/src/devAdmin.ts +78 -3
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/mcp.test.ts +25 -25
- package/packages/core/src/mcp.ts +112 -47
- package/packages/core/src/server.ts +7 -0
- package/packages/orm/src/database.ts +27 -11
package/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.35)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
@@ -1133,7 +1133,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
|
|
|
1133
1133
|
- **QueryBuilder** with NoSQL/MongoDB support (`toMongo()`)
|
|
1134
1134
|
- **WebSocket backplane** (Redis pub/sub) for horizontal scaling
|
|
1135
1135
|
- **SameSite=Lax** default on session cookies (`TINA4_SESSION_SAMESITE`)
|
|
1136
|
-
- **`tina4
|
|
1136
|
+
- **`tina4 deploy docker`** generates Dockerfile and .dockerignore
|
|
1137
1137
|
- **Gallery**: 7 interactive examples with Try It deploy at `/_dev/`
|
|
1138
1138
|
- **SSE/Streaming**: `response.stream()` for Server-Sent Events — pass an async generator, framework handles chunked transfer encoding and keep-alive
|
|
1139
1139
|
|
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ import { DevMailbox } from "./devMailbox.js";
|
|
|
19
19
|
import { isTruthy } from "./dotenv.js";
|
|
20
20
|
import { quickMetrics, fullAnalysis, fileDetail } from "./metrics.js";
|
|
21
21
|
import { registerFeedbackRoutes } from "./feedback.js";
|
|
22
|
+
import { getDefaultDevServer } from "./mcp.js";
|
|
22
23
|
|
|
23
24
|
const cpuCount = osCpus().length;
|
|
24
25
|
|
|
@@ -560,9 +561,18 @@ export class DevAdmin {
|
|
|
560
561
|
{ method: "POST", pattern: "/__dev/api/deps/install", handler: handleDepsInstall },
|
|
561
562
|
// Git status
|
|
562
563
|
{ method: "GET", pattern: "/__dev/api/git/status", handler: handleGitStatus },
|
|
563
|
-
// MCP tool introspection over the built-in MCP server
|
|
564
|
+
// MCP tool introspection over the built-in MCP server (browser dev-admin REST shim)
|
|
564
565
|
{ method: "GET", pattern: "/__dev/api/mcp/tools", handler: handleMcpTools },
|
|
565
566
|
{ method: "POST", pattern: "/__dev/api/mcp/call", handler: handleMcpCall },
|
|
567
|
+
// MCP JSON-RPC + SSE endpoints that REAL MCP clients (Claude Code/Desktop)
|
|
568
|
+
// speak. POST /__dev/mcp[/message] -> JSON-RPC handleMessage; GET
|
|
569
|
+
// /__dev/mcp/sse -> SSE handshake announcing the message endpoint. Mounted
|
|
570
|
+
// through the same dispatch as the REST shim above and gated by the same
|
|
571
|
+
// /__dev public-route rule. Mirrors the Python v3 fix (POST /__dev/mcp +
|
|
572
|
+
// /__dev/mcp/message, GET /__dev/mcp/sse).
|
|
573
|
+
{ method: "POST", pattern: "/__dev/mcp", handler: handleMcpMessage },
|
|
574
|
+
{ method: "POST", pattern: "/__dev/mcp/message", handler: handleMcpMessage },
|
|
575
|
+
{ method: "GET", pattern: "/__dev/mcp/sse", handler: handleMcpSse },
|
|
566
576
|
// Scaffolding
|
|
567
577
|
{ method: "GET", pattern: "/__dev/api/scaffold", handler: handleScaffoldList },
|
|
568
578
|
{ method: "POST", pattern: "/__dev/api/scaffold/run", handler: handleScaffoldRun },
|
|
@@ -599,6 +609,13 @@ export class DevAdmin {
|
|
|
599
609
|
handler: route.handler,
|
|
600
610
|
});
|
|
601
611
|
}
|
|
612
|
+
|
|
613
|
+
// Ensure the default /__dev/mcp MCP server exists with its dev tools
|
|
614
|
+
// registered. This is the single shared instance behind both the REST shim
|
|
615
|
+
// and the JSON-RPC + SSE endpoints registered above. Doing it here (gated by
|
|
616
|
+
// the same TINA4_DEBUG check that gates DevAdmin.register) means tools/list
|
|
617
|
+
// and the REST shim return tools immediately, before any first call.
|
|
618
|
+
getDefaultDevServer();
|
|
602
619
|
}
|
|
603
620
|
|
|
604
621
|
/**
|
|
@@ -1931,7 +1948,11 @@ const handleGitStatus: RouteHandler = async (_req, res) => {
|
|
|
1931
1948
|
|
|
1932
1949
|
const handleMcpTools: RouteHandler = async (_req, res) => {
|
|
1933
1950
|
try {
|
|
1934
|
-
|
|
1951
|
+
// Ensure the default /__dev/mcp server exists with its dev tools registered,
|
|
1952
|
+
// then enumerate every registered MCP server instance (app-defined servers
|
|
1953
|
+
// register themselves on construction too).
|
|
1954
|
+
const { McpServer, getDefaultDevServer } = await import("./mcp.js");
|
|
1955
|
+
getDefaultDevServer();
|
|
1935
1956
|
const instances = (McpServer as unknown as { _instances: Array<any> })._instances || [];
|
|
1936
1957
|
const tools: Array<{ server: string; name: string; description: string; inputSchema: unknown }> = [];
|
|
1937
1958
|
for (const s of instances) {
|
|
@@ -1950,7 +1971,8 @@ const handleMcpCall: RouteHandler = async (req, res) => {
|
|
|
1950
1971
|
const name = (body.name as string) || "";
|
|
1951
1972
|
const args = (body.arguments as Record<string, unknown>) || {};
|
|
1952
1973
|
try {
|
|
1953
|
-
const { McpServer } = await import("./mcp.js");
|
|
1974
|
+
const { McpServer, getDefaultDevServer } = await import("./mcp.js");
|
|
1975
|
+
getDefaultDevServer();
|
|
1954
1976
|
const instances = (McpServer as unknown as { _instances: Array<any> })._instances || [];
|
|
1955
1977
|
for (const s of instances) {
|
|
1956
1978
|
const tool = ((s as any)._tools as Map<string, any>).get(name);
|
|
@@ -1966,6 +1988,59 @@ const handleMcpCall: RouteHandler = async (req, res) => {
|
|
|
1966
1988
|
}
|
|
1967
1989
|
};
|
|
1968
1990
|
|
|
1991
|
+
/**
|
|
1992
|
+
* JSON-RPC message endpoint for real MCP clients.
|
|
1993
|
+
*
|
|
1994
|
+
* Mounted at POST /__dev/mcp and POST /__dev/mcp/message. Forwards the request
|
|
1995
|
+
* body to the default dev MCP server's handleMessage() and returns the JSON-RPC
|
|
1996
|
+
* response. Notifications / id-less requests yield an empty 204. Mirrors the
|
|
1997
|
+
* Python v3 fix. The /__dev path is always public (auth-bypassed), so MCP
|
|
1998
|
+
* clients connect without a token.
|
|
1999
|
+
*/
|
|
2000
|
+
const handleMcpMessage: RouteHandler = async (req, res) => {
|
|
2001
|
+
try {
|
|
2002
|
+
const { getDefaultDevServer } = await import("./mcp.js");
|
|
2003
|
+
const server = getDefaultDevServer();
|
|
2004
|
+
const body = req.body;
|
|
2005
|
+
let raw: string | Record<string, unknown>;
|
|
2006
|
+
if (typeof body === "object" && body !== null) {
|
|
2007
|
+
raw = body as Record<string, unknown>;
|
|
2008
|
+
} else {
|
|
2009
|
+
raw = typeof body === "string" ? body : String(body ?? "");
|
|
2010
|
+
}
|
|
2011
|
+
const result = await server.handleMessage(raw);
|
|
2012
|
+
if (!result) {
|
|
2013
|
+
// Notification / no id — nothing to return.
|
|
2014
|
+
res.send("", 204);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
res.json(JSON.parse(result));
|
|
2018
|
+
} catch (e) {
|
|
2019
|
+
res.json({ error: (e as Error).message }, 500);
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* SSE handshake endpoint for real MCP clients.
|
|
2025
|
+
*
|
|
2026
|
+
* Mounted at GET /__dev/mcp/sse. Announces the JSON-RPC message endpoint via an
|
|
2027
|
+
* `endpoint` event, exactly like the canonical McpServer.registerRoutes() and
|
|
2028
|
+
* the Python v3 fix. Content-Type text/event-stream, status 200.
|
|
2029
|
+
*/
|
|
2030
|
+
const handleMcpSse: RouteHandler = async (req, res) => {
|
|
2031
|
+
// req.path is the path only (no query); turn /__dev/mcp/sse into the message
|
|
2032
|
+
// endpoint /__dev/mcp/message that the client should POST to.
|
|
2033
|
+
const reqPath = req.path || "/__dev/mcp/sse";
|
|
2034
|
+
const endpointUrl = reqPath.replace(/\/sse$/, "/message");
|
|
2035
|
+
const sseData = `event: endpoint\ndata: ${endpointUrl}\n\n`;
|
|
2036
|
+
res.raw.writeHead(200, {
|
|
2037
|
+
"Content-Type": "text/event-stream",
|
|
2038
|
+
"Cache-Control": "no-cache",
|
|
2039
|
+
Connection: "keep-alive",
|
|
2040
|
+
});
|
|
2041
|
+
res.raw.end(sseData);
|
|
2042
|
+
};
|
|
2043
|
+
|
|
1969
2044
|
const handleScaffoldList: RouteHandler = (_req, res) => {
|
|
1970
2045
|
res.json({
|
|
1971
2046
|
scaffolds: [
|
|
@@ -121,7 +121,7 @@ export type { WebSocketConnection } from "./websocketConnection.js";
|
|
|
121
121
|
export { RedisBackplane, NATSBackplane, createBackplane } from "./websocketBackplane.js";
|
|
122
122
|
export type { WebSocketBackplane } from "./websocketBackplane.js";
|
|
123
123
|
export {
|
|
124
|
-
McpServer, mcpTool, mcpResource, registerDevTools,
|
|
124
|
+
McpServer, mcpTool, mcpResource, registerDevTools, getDefaultDevServer,
|
|
125
125
|
encodeResponse, encodeError, encodeNotification, decodeRequest,
|
|
126
126
|
schemaFromParams, isLocalhost, mcpEnabled, mcpPort,
|
|
127
127
|
PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR,
|
|
@@ -116,7 +116,7 @@ console.log("\nMcpServer Core");
|
|
|
116
116
|
{
|
|
117
117
|
McpServer._instances = [];
|
|
118
118
|
const server = new McpServer("/test-mcp", "Test Server", "0.1.0");
|
|
119
|
-
const resp = JSON.parse(server.handleMessage({
|
|
119
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
120
120
|
jsonrpc: "2.0", id: 1, method: "initialize",
|
|
121
121
|
params: {
|
|
122
122
|
protocolVersion: "2024-11-05", capabilities: {},
|
|
@@ -131,7 +131,7 @@ console.log("\nMcpServer Core");
|
|
|
131
131
|
{
|
|
132
132
|
McpServer._instances = [];
|
|
133
133
|
const server = new McpServer("/test-mcp", "Test Server");
|
|
134
|
-
const resp = JSON.parse(server.handleMessage({
|
|
134
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
135
135
|
jsonrpc: "2.0", id: 2, method: "ping", params: {},
|
|
136
136
|
}));
|
|
137
137
|
assert("ping — result empty", JSON.stringify(resp.result) === "{}");
|
|
@@ -140,7 +140,7 @@ console.log("\nMcpServer Core");
|
|
|
140
140
|
{
|
|
141
141
|
McpServer._instances = [];
|
|
142
142
|
const server = new McpServer("/test-mcp", "Test Server");
|
|
143
|
-
const resp = JSON.parse(server.handleMessage({
|
|
143
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
144
144
|
jsonrpc: "2.0", id: 3, method: "nonexistent", params: {},
|
|
145
145
|
}));
|
|
146
146
|
assert("method not found — code", resp.error.code === -32601);
|
|
@@ -149,7 +149,7 @@ console.log("\nMcpServer Core");
|
|
|
149
149
|
{
|
|
150
150
|
McpServer._instances = [];
|
|
151
151
|
const server = new McpServer("/test-mcp", "Test Server");
|
|
152
|
-
const resp = server.handleMessage({
|
|
152
|
+
const resp = await server.handleMessage({
|
|
153
153
|
jsonrpc: "2.0", method: "notifications/initialized",
|
|
154
154
|
});
|
|
155
155
|
assert("notification — empty response", resp === "");
|
|
@@ -169,7 +169,7 @@ console.log("\nTool Registration and Call");
|
|
|
169
169
|
schemaFromParams([{ name: "name", type: "string" }]),
|
|
170
170
|
);
|
|
171
171
|
|
|
172
|
-
const resp = JSON.parse(server.handleMessage({
|
|
172
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
173
173
|
jsonrpc: "2.0", id: 1, method: "tools/list", params: {},
|
|
174
174
|
}));
|
|
175
175
|
const tools = resp.result.tools;
|
|
@@ -189,7 +189,7 @@ console.log("\nTool Registration and Call");
|
|
|
189
189
|
schemaFromParams([{ name: "a", type: "integer" }, { name: "b", type: "integer" }]),
|
|
190
190
|
);
|
|
191
191
|
|
|
192
|
-
const resp = JSON.parse(server.handleMessage({
|
|
192
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
193
193
|
jsonrpc: "2.0", id: 2, method: "tools/call",
|
|
194
194
|
params: { name: "add", arguments: { a: 3, b: 5 } },
|
|
195
195
|
}));
|
|
@@ -202,7 +202,7 @@ console.log("\nTool Registration and Call");
|
|
|
202
202
|
{
|
|
203
203
|
McpServer._instances = [];
|
|
204
204
|
const server = new McpServer("/test-tools", "Tool Test");
|
|
205
|
-
const resp = JSON.parse(server.handleMessage({
|
|
205
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
206
206
|
jsonrpc: "2.0", id: 3, method: "tools/call",
|
|
207
207
|
params: { name: "missing", arguments: {} },
|
|
208
208
|
}));
|
|
@@ -215,7 +215,7 @@ console.log("\nTool Registration and Call");
|
|
|
215
215
|
server.registerTool("echo", (args) => args.msg as string, "Echo",
|
|
216
216
|
schemaFromParams([{ name: "msg", type: "string" }]));
|
|
217
217
|
|
|
218
|
-
const resp = JSON.parse(server.handleMessage({
|
|
218
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
219
219
|
jsonrpc: "2.0", id: 4, method: "tools/call",
|
|
220
220
|
params: { name: "echo", arguments: { msg: "hello" } },
|
|
221
221
|
}));
|
|
@@ -227,7 +227,7 @@ console.log("\nTool Registration and Call");
|
|
|
227
227
|
const server = new McpServer("/test-tools", "Tool Test");
|
|
228
228
|
server.registerTool("data", () => ({ a: 1, b: 2 }), "Return data");
|
|
229
229
|
|
|
230
|
-
const resp = JSON.parse(server.handleMessage({
|
|
230
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
231
231
|
jsonrpc: "2.0", id: 5, method: "tools/call",
|
|
232
232
|
params: { name: "data", arguments: {} },
|
|
233
233
|
}));
|
|
@@ -262,7 +262,7 @@ console.log("\nResource Registration and Read");
|
|
|
262
262
|
const server = new McpServer("/test-resources", "Resource Test");
|
|
263
263
|
server.registerResource("app://tables", () => ["users", "products"], "Database tables");
|
|
264
264
|
|
|
265
|
-
const resp = JSON.parse(server.handleMessage({
|
|
265
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
266
266
|
jsonrpc: "2.0", id: 1, method: "resources/list", params: {},
|
|
267
267
|
}));
|
|
268
268
|
const resources = resp.result.resources;
|
|
@@ -275,7 +275,7 @@ console.log("\nResource Registration and Read");
|
|
|
275
275
|
const server = new McpServer("/test-resources", "Resource Test");
|
|
276
276
|
server.registerResource("app://info", () => ({ version: "1.0", name: "Test App" }), "App info");
|
|
277
277
|
|
|
278
|
-
const resp = JSON.parse(server.handleMessage({
|
|
278
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
279
279
|
jsonrpc: "2.0", id: 2, method: "resources/read",
|
|
280
280
|
params: { uri: "app://info" },
|
|
281
281
|
}));
|
|
@@ -288,7 +288,7 @@ console.log("\nResource Registration and Read");
|
|
|
288
288
|
{
|
|
289
289
|
McpServer._instances = [];
|
|
290
290
|
const server = new McpServer("/test-resources", "Resource Test");
|
|
291
|
-
const resp = JSON.parse(server.handleMessage({
|
|
291
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
292
292
|
jsonrpc: "2.0", id: 3, method: "resources/read",
|
|
293
293
|
params: { uri: "app://missing" },
|
|
294
294
|
}));
|
|
@@ -335,7 +335,7 @@ console.log("\nFile Sandbox");
|
|
|
335
335
|
const server = new McpServer("/test-sandbox", "Sandbox Test");
|
|
336
336
|
registerDevTools(server);
|
|
337
337
|
|
|
338
|
-
const resp = JSON.parse(server.handleMessage({
|
|
338
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
339
339
|
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
340
340
|
params: { name: "file_read", arguments: { path: "../../../etc/passwd" } },
|
|
341
341
|
}));
|
|
@@ -361,7 +361,7 @@ console.log("\nFile Sandbox");
|
|
|
361
361
|
const server = new McpServer("/test-sandbox2", "Sandbox Test 2");
|
|
362
362
|
registerDevTools(server);
|
|
363
363
|
|
|
364
|
-
const resp = JSON.parse(server.handleMessage({
|
|
364
|
+
const resp = JSON.parse(await server.handleMessage({
|
|
365
365
|
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
366
366
|
params: { name: "file_write", arguments: { path: "../../evil.txt", content: "hacked" } },
|
|
367
367
|
}));
|
|
@@ -401,11 +401,11 @@ console.log("\nDefensive Write Helpers");
|
|
|
401
401
|
* For string returns the inner text is the raw string; for object returns
|
|
402
402
|
* it is JSON-encoded — try to parse it back, fall through to the raw text.
|
|
403
403
|
*/
|
|
404
|
-
function callTool(server: McpServer, name: string, args: Record<string, unknown>): {
|
|
404
|
+
async function callTool(server: McpServer, name: string, args: Record<string, unknown>): Promise<{
|
|
405
405
|
rpc: { jsonrpc?: string; id?: unknown; result?: unknown; error?: { code: number; message: string } };
|
|
406
406
|
result: Record<string, unknown> | string | null;
|
|
407
|
-
} {
|
|
408
|
-
const rpc = JSON.parse(server.handleMessage({
|
|
407
|
+
}> {
|
|
408
|
+
const rpc = JSON.parse(await server.handleMessage({
|
|
409
409
|
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
410
410
|
params: { name, arguments: args },
|
|
411
411
|
}));
|
|
@@ -431,7 +431,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
431
431
|
try {
|
|
432
432
|
const server = new McpServer("/test-prose", "Prose Test");
|
|
433
433
|
registerDevTools(server);
|
|
434
|
-
const { result } = callTool(server, "file_write", {
|
|
434
|
+
const { result } = await callTool(server, "file_write", {
|
|
435
435
|
path: "The plan requires implementing a new feature for users.ts",
|
|
436
436
|
content: "x",
|
|
437
437
|
});
|
|
@@ -455,7 +455,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
455
455
|
try {
|
|
456
456
|
const server = new McpServer("/test-normalize", "Normalize Test");
|
|
457
457
|
registerDevTools(server);
|
|
458
|
-
const { result } = callTool(server, "file_write", {
|
|
458
|
+
const { result } = await callTool(server, "file_write", {
|
|
459
459
|
path: "routes/foo.ts",
|
|
460
460
|
content: "export default async function (req, res) {}\n",
|
|
461
461
|
});
|
|
@@ -492,7 +492,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
492
492
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
493
493
|
fs.writeFileSync(target, "original content\n", "utf-8");
|
|
494
494
|
|
|
495
|
-
const { result } = callTool(server, "file_write", {
|
|
495
|
+
const { result } = await callTool(server, "file_write", {
|
|
496
496
|
path: "src/routes/foo.ts",
|
|
497
497
|
content: "new content that is reasonably similar in length to the old one\n",
|
|
498
498
|
});
|
|
@@ -531,7 +531,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
531
531
|
fs.writeFileSync(target, original, "utf-8");
|
|
532
532
|
|
|
533
533
|
// Overwrite with 50 bytes — should be REFUSED (50/500 = 10% < 30%)
|
|
534
|
-
const { result } = callTool(server, "file_write", {
|
|
534
|
+
const { result } = await callTool(server, "file_write", {
|
|
535
535
|
path: "src/routes/big.ts",
|
|
536
536
|
content: "y".repeat(50),
|
|
537
537
|
});
|
|
@@ -562,7 +562,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
562
562
|
try {
|
|
563
563
|
const server = new McpServer("/test-passthrough", "Passthrough Test");
|
|
564
564
|
registerDevTools(server);
|
|
565
|
-
const { result } = callTool(server, "file_write", {
|
|
565
|
+
const { result } = await callTool(server, "file_write", {
|
|
566
566
|
path: "src/routes/foo.ts",
|
|
567
567
|
content: "export default async function (req, res) {}\n",
|
|
568
568
|
});
|
|
@@ -602,7 +602,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
602
602
|
try {
|
|
603
603
|
const server = new McpServer("/test-verify-js", "Verify JS");
|
|
604
604
|
registerDevTools(server);
|
|
605
|
-
const { result } = callTool(server, "file_write", {
|
|
605
|
+
const { result } = await callTool(server, "file_write", {
|
|
606
606
|
path: "src/routes/broken.js",
|
|
607
607
|
content: "const x = ;\n",
|
|
608
608
|
});
|
|
@@ -635,7 +635,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
635
635
|
const server = new McpServer("/test-verify-skip-ext", "Verify Skip Ext");
|
|
636
636
|
registerDevTools(server);
|
|
637
637
|
// .twig content that would obviously fail any JS parser
|
|
638
|
-
const { result } = callTool(server, "file_write", {
|
|
638
|
+
const { result } = await callTool(server, "file_write", {
|
|
639
639
|
path: "src/templates/page.twig",
|
|
640
640
|
content: "{% if not valid js %}<h1>Hi</h1>{% endif %}\n",
|
|
641
641
|
});
|
|
@@ -668,7 +668,7 @@ function callTool(server: McpServer, name: string, args: Record<string, unknown>
|
|
|
668
668
|
const server = new McpServer("/test-verify-skip-outside", "Verify Skip Outside");
|
|
669
669
|
registerDevTools(server);
|
|
670
670
|
// Broken JS placed under tests/ — must NOT be checked
|
|
671
|
-
const { result } = callTool(server, "file_write", {
|
|
671
|
+
const { result } = await callTool(server, "file_write", {
|
|
672
672
|
path: "tests/foo.js",
|
|
673
673
|
content: "const x = ;\n",
|
|
674
674
|
});
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -18,6 +18,32 @@ import * as fs from "node:fs";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import * as os from "node:os";
|
|
20
20
|
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { createRequire } from "node:module";
|
|
22
|
+
|
|
23
|
+
// Synchronous CommonJS-style require that works under real ESM (where the
|
|
24
|
+
// bare `require` global is undefined). Dev-tool handlers are synchronous, so
|
|
25
|
+
// they can't `await import()` — this gives them a working require. Mirrors the
|
|
26
|
+
// pattern already used by the ORM adapters (mysql.ts, postgres.ts, etc.).
|
|
27
|
+
const req = createRequire(import.meta.url);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Require a sibling @tina4 workspace package (orm / swagger / frond) in a way
|
|
31
|
+
* that works whether we're running from source (monorepo, where the package's
|
|
32
|
+
* `exports` only exposes the TS `import` condition that `require()` can't see)
|
|
33
|
+
* or as an installed dependency (where the package name resolves directly).
|
|
34
|
+
*
|
|
35
|
+
* Tries the package name first; on failure falls back to the in-repo source
|
|
36
|
+
* path relative to this file (packages/core/src → packages/<name>/src). This
|
|
37
|
+
* is why `route_list`, `database_query`, `swagger_spec`, etc. work when a real
|
|
38
|
+
* MCP client hits /__dev/mcp from a from-source dev server.
|
|
39
|
+
*/
|
|
40
|
+
function reqSibling(pkg: "orm" | "swagger" | "frond"): Record<string, unknown> {
|
|
41
|
+
try {
|
|
42
|
+
return req(`@tina4/${pkg}`) as Record<string, unknown>;
|
|
43
|
+
} catch {
|
|
44
|
+
return req(`../../${pkg}/src/index.ts`) as Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
21
47
|
|
|
22
48
|
// ── Types ─────────────────────────────────────────────────────
|
|
23
49
|
|
|
@@ -34,7 +60,11 @@ export interface McpToolDefinition {
|
|
|
34
60
|
name: string;
|
|
35
61
|
description: string;
|
|
36
62
|
inputSchema: JsonSchema;
|
|
37
|
-
|
|
63
|
+
// Handlers may be sync or async — the dispatch (`_handleToolsCall`) awaits the
|
|
64
|
+
// return value, so DB tools that hit the async Database wrapper resolve before
|
|
65
|
+
// the result is formatted. A sync handler's plain return passes through awaiting
|
|
66
|
+
// unchanged.
|
|
67
|
+
handler: (args: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
export interface McpResourceDefinition {
|
|
@@ -251,7 +281,13 @@ export class McpServer {
|
|
|
251
281
|
});
|
|
252
282
|
}
|
|
253
283
|
|
|
254
|
-
|
|
284
|
+
// Async since the DB dev-tool handlers (database_query/execute/tables/columns,
|
|
285
|
+
// migration_*, seed_table, project_overview) reach the Database wrapper on
|
|
286
|
+
// `globalThis.__tina4_db`, whose read/write methods are async. The handler is
|
|
287
|
+
// awaited below; sync handlers (the file/plan/route tools) are unaffected
|
|
288
|
+
// because awaiting a non-Promise resolves immediately. Returns a
|
|
289
|
+
// Promise<string> — every caller must await it.
|
|
290
|
+
async handleMessage(rawData: string | Record<string, unknown>): Promise<string> {
|
|
255
291
|
let method: string;
|
|
256
292
|
let params: Record<string, unknown>;
|
|
257
293
|
let requestId: number | string | null;
|
|
@@ -278,7 +314,7 @@ export class McpServer {
|
|
|
278
314
|
}
|
|
279
315
|
|
|
280
316
|
try {
|
|
281
|
-
const result = handler(params);
|
|
317
|
+
const result = await handler(params);
|
|
282
318
|
if (requestId === null) {
|
|
283
319
|
return ""; // Notification — no response
|
|
284
320
|
}
|
|
@@ -323,7 +359,7 @@ export class McpServer {
|
|
|
323
359
|
return { tools };
|
|
324
360
|
}
|
|
325
361
|
|
|
326
|
-
private _handleToolsCall(params: Record<string, unknown>): Record<string, unknown
|
|
362
|
+
private async _handleToolsCall(params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
327
363
|
const toolName = params.name as string | undefined;
|
|
328
364
|
if (!toolName) {
|
|
329
365
|
throw new Error("Missing tool name");
|
|
@@ -335,7 +371,9 @@ export class McpServer {
|
|
|
335
371
|
}
|
|
336
372
|
|
|
337
373
|
const args = (params.arguments as Record<string, unknown>) || {};
|
|
338
|
-
|
|
374
|
+
// Tool handlers may be async (the DB tools await the Database wrapper); a
|
|
375
|
+
// sync handler's plain return value passes through `await` unchanged.
|
|
376
|
+
const result = await tool.handler(args);
|
|
339
377
|
|
|
340
378
|
// Format result as MCP content
|
|
341
379
|
let content: { type: string; text: string }[];
|
|
@@ -412,7 +450,7 @@ export class McpServer {
|
|
|
412
450
|
const ssePath = `${this.path}/sse`;
|
|
413
451
|
|
|
414
452
|
router
|
|
415
|
-
.post(msgPath, (req: unknown, res: unknown) => {
|
|
453
|
+
.post(msgPath, async (req: unknown, res: unknown) => {
|
|
416
454
|
const request = req as { body: unknown; url?: string };
|
|
417
455
|
const response = res as ((data: unknown, status?: number, contentType?: string) => unknown);
|
|
418
456
|
const body = request.body;
|
|
@@ -422,7 +460,7 @@ export class McpServer {
|
|
|
422
460
|
} else {
|
|
423
461
|
raw = typeof body === "string" ? body : String(body);
|
|
424
462
|
}
|
|
425
|
-
const result = server.handleMessage(raw);
|
|
463
|
+
const result = await server.handleMessage(raw);
|
|
426
464
|
if (!result) {
|
|
427
465
|
return response("", 204);
|
|
428
466
|
}
|
|
@@ -484,6 +522,7 @@ export class McpServer {
|
|
|
484
522
|
// ── Decorator API ──────────────────────────────────────────────
|
|
485
523
|
|
|
486
524
|
let _defaultServer: McpServer | null = null;
|
|
525
|
+
let _defaultToolsRegistered = false;
|
|
487
526
|
|
|
488
527
|
function _getDefaultServer(): McpServer {
|
|
489
528
|
if (_defaultServer === null) {
|
|
@@ -492,6 +531,24 @@ function _getDefaultServer(): McpServer {
|
|
|
492
531
|
return _defaultServer;
|
|
493
532
|
}
|
|
494
533
|
|
|
534
|
+
/**
|
|
535
|
+
* The default `/__dev/mcp` MCP server with the built-in dev tools registered.
|
|
536
|
+
*
|
|
537
|
+
* This is the single shared instance backing BOTH the browser REST shim
|
|
538
|
+
* (`/__dev/api/mcp/tools` + `/__dev/api/mcp/call`) and the JSON-RPC + SSE
|
|
539
|
+
* endpoints (`/__dev/mcp[/message]` + `/__dev/mcp/sse`) that real MCP clients
|
|
540
|
+
* (Claude Code/Desktop) speak. Tools are registered exactly once (idempotent).
|
|
541
|
+
* Mirrors Python's default MCP server used by `get_api_handlers()`.
|
|
542
|
+
*/
|
|
543
|
+
export function getDefaultDevServer(): McpServer {
|
|
544
|
+
const server = _getDefaultServer();
|
|
545
|
+
if (!_defaultToolsRegistered) {
|
|
546
|
+
registerDevTools(server);
|
|
547
|
+
_defaultToolsRegistered = true;
|
|
548
|
+
}
|
|
549
|
+
return server;
|
|
550
|
+
}
|
|
551
|
+
|
|
495
552
|
/**
|
|
496
553
|
* Register a function as an MCP tool.
|
|
497
554
|
*
|
|
@@ -787,13 +844,12 @@ export function registerDevTools(server: McpServer): void {
|
|
|
787
844
|
|
|
788
845
|
server.registerTool(
|
|
789
846
|
"database_query",
|
|
790
|
-
(args) => {
|
|
847
|
+
async (args) => {
|
|
791
848
|
try {
|
|
792
|
-
const { initDatabase } = require("@tina4/orm");
|
|
793
849
|
const db = (globalThis as any).__tina4_db;
|
|
794
850
|
if (!db) return { error: "No database connection" };
|
|
795
851
|
const params = typeof args.params === "string" ? JSON.parse(args.params as string) : (args.params || []);
|
|
796
|
-
const result = db.fetch(args.sql as string, params);
|
|
852
|
+
const result = await db.fetch(args.sql as string, params);
|
|
797
853
|
return { records: result.records || [], count: result.count || 0 };
|
|
798
854
|
} catch (e) {
|
|
799
855
|
return { error: (e as Error).message };
|
|
@@ -808,14 +864,14 @@ export function registerDevTools(server: McpServer): void {
|
|
|
808
864
|
|
|
809
865
|
server.registerTool(
|
|
810
866
|
"database_execute",
|
|
811
|
-
(args) => {
|
|
867
|
+
async (args) => {
|
|
812
868
|
try {
|
|
813
869
|
const db = (globalThis as any).__tina4_db;
|
|
814
870
|
if (!db) return { error: "No database connection" };
|
|
815
871
|
const params = typeof args.params === "string" ? JSON.parse(args.params as string) : (args.params || []);
|
|
816
|
-
const result = db.execute(args.sql as string, params);
|
|
817
|
-
db.commit?.();
|
|
818
|
-
return { success: true, affected_rows: result?.count ?? 0 };
|
|
872
|
+
const result = await db.execute(args.sql as string, params);
|
|
873
|
+
await db.commit?.();
|
|
874
|
+
return { success: true, affected_rows: (result as any)?.count ?? 0 };
|
|
819
875
|
} catch (e) {
|
|
820
876
|
return { error: (e as Error).message };
|
|
821
877
|
}
|
|
@@ -829,11 +885,11 @@ export function registerDevTools(server: McpServer): void {
|
|
|
829
885
|
|
|
830
886
|
server.registerTool(
|
|
831
887
|
"database_tables",
|
|
832
|
-
(_args) => {
|
|
888
|
+
async (_args) => {
|
|
833
889
|
try {
|
|
834
890
|
const db = (globalThis as any).__tina4_db;
|
|
835
891
|
if (!db) return { error: "No database connection" };
|
|
836
|
-
return db.getTables?.() ?? [];
|
|
892
|
+
return (await db.getTables?.()) ?? [];
|
|
837
893
|
} catch (e) {
|
|
838
894
|
return { error: (e as Error).message };
|
|
839
895
|
}
|
|
@@ -844,11 +900,11 @@ export function registerDevTools(server: McpServer): void {
|
|
|
844
900
|
|
|
845
901
|
server.registerTool(
|
|
846
902
|
"database_columns",
|
|
847
|
-
(args) => {
|
|
903
|
+
async (args) => {
|
|
848
904
|
try {
|
|
849
905
|
const db = (globalThis as any).__tina4_db;
|
|
850
906
|
if (!db) return { error: "No database connection" };
|
|
851
|
-
return db.getColumns?.(args.table as string) ?? [];
|
|
907
|
+
return (await db.getColumns?.(args.table as string)) ?? [];
|
|
852
908
|
} catch (e) {
|
|
853
909
|
return { error: (e as Error).message };
|
|
854
910
|
}
|
|
@@ -863,8 +919,16 @@ export function registerDevTools(server: McpServer): void {
|
|
|
863
919
|
"route_list",
|
|
864
920
|
(_args) => {
|
|
865
921
|
try {
|
|
866
|
-
|
|
867
|
-
|
|
922
|
+
// Prefer the active server router (set by startServer) so file-discovered
|
|
923
|
+
// routes are included; startServer builds a fresh Router rather than using
|
|
924
|
+
// defaultRouter. Fall back to defaultRouter when no server is running.
|
|
925
|
+
// route_list lives inside @tina4/core, so reach the router via the local
|
|
926
|
+
// module — req("@tina4/core") depends on a built dist/ and fails under a
|
|
927
|
+
// from-source ESM runtime (the real MCP-client code path).
|
|
928
|
+
const { defaultRouter } = req("./router.js") as typeof import("./router.js");
|
|
929
|
+
const activeRouter = (globalThis as any).__tina4_router;
|
|
930
|
+
const router = activeRouter ?? defaultRouter;
|
|
931
|
+
const routes = router?.listRoutes?.() ?? [];
|
|
868
932
|
return routes.map((r: any) => ({
|
|
869
933
|
method: r.method || "",
|
|
870
934
|
path: r.pattern || r.path || "",
|
|
@@ -901,10 +965,12 @@ export function registerDevTools(server: McpServer): void {
|
|
|
901
965
|
"swagger_spec",
|
|
902
966
|
(_args) => {
|
|
903
967
|
try {
|
|
904
|
-
const {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
return { info: "Swagger
|
|
968
|
+
const { generate } = reqSibling("swagger") as { generate?: (routes: unknown[], models?: unknown) => unknown };
|
|
969
|
+
const { defaultRouter } = req("./router.js") as typeof import("./router.js");
|
|
970
|
+
const routes = defaultRouter?.getRoutes?.() ?? [];
|
|
971
|
+
return generate?.(routes, []) ?? { info: "Swagger not available" };
|
|
972
|
+
} catch (e) {
|
|
973
|
+
return { error: (e as Error).message };
|
|
908
974
|
}
|
|
909
975
|
},
|
|
910
976
|
"Return the OpenAPI 3.0.3 JSON spec",
|
|
@@ -917,9 +983,11 @@ export function registerDevTools(server: McpServer): void {
|
|
|
917
983
|
"template_render",
|
|
918
984
|
(args) => {
|
|
919
985
|
try {
|
|
920
|
-
const {
|
|
986
|
+
const { Frond } = reqSibling("frond") as { Frond?: new (dir?: string) => { renderString: (s: string, d?: Record<string, unknown>) => string } };
|
|
987
|
+
if (!Frond) return "Template engine not available";
|
|
921
988
|
const data = typeof args.data === "string" ? JSON.parse(args.data as string) : (args.data || {});
|
|
922
|
-
|
|
989
|
+
const frond = new Frond(path.join(projectRoot, "src", "templates"));
|
|
990
|
+
return frond.renderString(args.template as string, data as Record<string, unknown>);
|
|
923
991
|
} catch (e) {
|
|
924
992
|
return { error: (e as Error).message };
|
|
925
993
|
}
|
|
@@ -1066,7 +1134,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1066
1134
|
|
|
1067
1135
|
server.registerTool(
|
|
1068
1136
|
"migration_status",
|
|
1069
|
-
(_args) => {
|
|
1137
|
+
async (_args) => {
|
|
1070
1138
|
try {
|
|
1071
1139
|
const db = (globalThis as any).__tina4_db;
|
|
1072
1140
|
if (!db) return { error: "No database connection" };
|
|
@@ -1099,7 +1167,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1099
1167
|
|
|
1100
1168
|
server.registerTool(
|
|
1101
1169
|
"migration_run",
|
|
1102
|
-
(_args) => {
|
|
1170
|
+
async (_args) => {
|
|
1103
1171
|
try {
|
|
1104
1172
|
const db = (globalThis as any).__tina4_db;
|
|
1105
1173
|
if (!db) return { error: "No database connection" };
|
|
@@ -1118,7 +1186,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1118
1186
|
"queue_status",
|
|
1119
1187
|
(args) => {
|
|
1120
1188
|
try {
|
|
1121
|
-
const { Queue } =
|
|
1189
|
+
const { Queue } = req("./queue.js") as typeof import("./queue.js");
|
|
1122
1190
|
const topic = (args.topic as string) || "default";
|
|
1123
1191
|
const q = new Queue({ topic });
|
|
1124
1192
|
return {
|
|
@@ -1166,7 +1234,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1166
1234
|
// latest snapshot once available. The very first call may report the
|
|
1167
1235
|
// pending placeholder; subsequent calls return live figures.
|
|
1168
1236
|
try {
|
|
1169
|
-
const mod =
|
|
1237
|
+
const mod = req("./cache.js") as typeof import("./cache.js");
|
|
1170
1238
|
const stats = mod.cacheStats?.();
|
|
1171
1239
|
if (stats && typeof stats.then === "function") {
|
|
1172
1240
|
stats.then((s: unknown) => { _lastCacheStats = s as Record<string, unknown>; }).catch(() => {});
|
|
@@ -1222,12 +1290,9 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1222
1290
|
"error_log",
|
|
1223
1291
|
(args) => {
|
|
1224
1292
|
try {
|
|
1225
|
-
const {
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
return tracker.get(args.limit || 20);
|
|
1229
|
-
}
|
|
1230
|
-
return [];
|
|
1293
|
+
const { ErrorTracker } = req("./devAdmin.js") as typeof import("./devAdmin.js");
|
|
1294
|
+
const limit = (args.limit as number) || 20;
|
|
1295
|
+
return ErrorTracker.get().slice(0, limit);
|
|
1231
1296
|
} catch {
|
|
1232
1297
|
return [];
|
|
1233
1298
|
}
|
|
@@ -1256,13 +1321,13 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1256
1321
|
|
|
1257
1322
|
server.registerTool(
|
|
1258
1323
|
"seed_table",
|
|
1259
|
-
(args) => {
|
|
1324
|
+
async (args) => {
|
|
1260
1325
|
try {
|
|
1261
|
-
const { seedTable } =
|
|
1326
|
+
const { seedTable } = reqSibling("orm") as { seedTable?: (db: unknown, table: string, count: number) => number | Promise<number> };
|
|
1262
1327
|
const db = (globalThis as any).__tina4_db;
|
|
1263
1328
|
if (!db) return { error: "No database connection" };
|
|
1264
1329
|
const count = (args.count as number) || 10;
|
|
1265
|
-
const inserted = seedTable?.(db, args.table as string, count) ?? 0;
|
|
1330
|
+
const inserted = (await seedTable?.(db, args.table as string, count)) ?? 0;
|
|
1266
1331
|
return { table: args.table, inserted };
|
|
1267
1332
|
} catch (e) {
|
|
1268
1333
|
return { error: (e as Error).message };
|
|
@@ -1305,9 +1370,9 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1305
1370
|
// Ported from Python's tina4_python.mcp.tools — names match exactly.
|
|
1306
1371
|
// The Plan storage format is byte-for-byte compatible across frameworks.
|
|
1307
1372
|
|
|
1308
|
-
const loadPlan = () =>
|
|
1373
|
+
const loadPlan = () => req("./plan.js").Plan as typeof import("./plan.js").Plan;
|
|
1309
1374
|
const loadIndex = () =>
|
|
1310
|
-
|
|
1375
|
+
req("./projectIndex.js").ProjectIndex as typeof import("./projectIndex.js").ProjectIndex;
|
|
1311
1376
|
|
|
1312
1377
|
server.registerTool(
|
|
1313
1378
|
"plan_current",
|
|
@@ -1429,14 +1494,14 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1429
1494
|
|
|
1430
1495
|
server.registerTool(
|
|
1431
1496
|
"project_overview",
|
|
1432
|
-
() => {
|
|
1497
|
+
async () => {
|
|
1433
1498
|
const out: Record<string, unknown> = {};
|
|
1434
1499
|
try { out.index = loadIndex().overview(); } catch (e) { out.index = { error: (e as Error).message }; }
|
|
1435
1500
|
try { out.plans = loadPlan().listPlans(); } catch (e) { out.plans = { error: (e as Error).message }; }
|
|
1436
1501
|
try { out.current_plan = loadPlan().current(); } catch (e) { out.current_plan = { error: (e as Error).message }; }
|
|
1437
1502
|
try {
|
|
1438
1503
|
const db = (globalThis as any).__tina4_db;
|
|
1439
|
-
out.tables = db?.getTables?.() ?? [];
|
|
1504
|
+
out.tables = (await db?.getTables?.()) ?? [];
|
|
1440
1505
|
} catch (e) { out.tables = { error: (e as Error).message }; }
|
|
1441
1506
|
return out;
|
|
1442
1507
|
},
|
|
@@ -1633,7 +1698,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1633
1698
|
"git_status",
|
|
1634
1699
|
() => {
|
|
1635
1700
|
try {
|
|
1636
|
-
const { execFileSync } =
|
|
1701
|
+
const { execFileSync } = req("node:child_process") as typeof import("node:child_process");
|
|
1637
1702
|
const cwd = path.resolve(process.cwd());
|
|
1638
1703
|
const run = (args: string[]): string => {
|
|
1639
1704
|
return execFileSync("git", args, { cwd, timeout: 3000, encoding: "utf-8" }).toString().trim();
|
|
@@ -1685,7 +1750,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1685
1750
|
(args) => {
|
|
1686
1751
|
try {
|
|
1687
1752
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1688
|
-
const { Docs } =
|
|
1753
|
+
const { Docs } = req("./docs.js") as typeof import("./docs.js");
|
|
1689
1754
|
return Docs.mcpSearch(
|
|
1690
1755
|
(args.query as string) || "",
|
|
1691
1756
|
parseInt(String(args.k ?? 5), 10) || 5,
|
|
@@ -1711,7 +1776,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1711
1776
|
(args) => {
|
|
1712
1777
|
try {
|
|
1713
1778
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1714
|
-
const { Docs } =
|
|
1779
|
+
const { Docs } = req("./docs.js") as typeof import("./docs.js");
|
|
1715
1780
|
const spec = Docs.mcpClass((args.name as string) || "");
|
|
1716
1781
|
return spec ?? { error: `class not found: ${args.name}` };
|
|
1717
1782
|
} catch (e) {
|
|
@@ -1727,7 +1792,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1727
1792
|
(args) => {
|
|
1728
1793
|
try {
|
|
1729
1794
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1730
|
-
const { Docs } =
|
|
1795
|
+
const { Docs } = req("./docs.js") as typeof import("./docs.js");
|
|
1731
1796
|
// PHP names the param `class`, Python names it `class_` — Node.js MCP
|
|
1732
1797
|
// accepts the raw `class` field from the JSON-RPC payload.
|
|
1733
1798
|
const cls = (args.class as string) || (args.class_name as string) || "";
|
|
@@ -756,6 +756,13 @@ ${reset}
|
|
|
756
756
|
const router = new Router();
|
|
757
757
|
const middleware = new MiddlewareChain();
|
|
758
758
|
|
|
759
|
+
// Expose the active server router globally so dev tools (e.g. the MCP
|
|
760
|
+
// route_list tool) can introspect the real, fully-populated route table —
|
|
761
|
+
// startServer builds a fresh Router rather than using defaultRouter, so
|
|
762
|
+
// file-discovered routes only live here. Mirrors the globalThis.__tina4_db
|
|
763
|
+
// hook other dev tools read.
|
|
764
|
+
(globalThis as any).__tina4_router = router;
|
|
765
|
+
|
|
759
766
|
// Merge routes registered via top-level get(), post(), etc.
|
|
760
767
|
for (const route of defaultRouter.getRoutes()) {
|
|
761
768
|
router.addRoute(route);
|
|
@@ -161,6 +161,21 @@ export function setAdapter(adapter: DatabaseAdapter): DatabaseAdapter {
|
|
|
161
161
|
return activeAdapter;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Publish the live Database wrapper on `globalThis.__tina4_db` so framework
|
|
166
|
+
* tooling that runs outside the request/ORM path can reach it. The built-in
|
|
167
|
+
* MCP dev-tool handlers (database_query/execute/tables/columns, migration_*,
|
|
168
|
+
* seed_table, project_overview in `@tina4/core`'s mcp.ts) read this global; it
|
|
169
|
+
* is the single chokepoint every `initDatabase()` / `Database.create()` return
|
|
170
|
+
* path flows through, so the global is always set once a connection exists.
|
|
171
|
+
*
|
|
172
|
+
* Returns the same instance so it composes cleanly around a `return`.
|
|
173
|
+
*/
|
|
174
|
+
function exposeDb(db: Database): Database {
|
|
175
|
+
(globalThis as any).__tina4_db = db;
|
|
176
|
+
return db;
|
|
177
|
+
}
|
|
178
|
+
|
|
164
179
|
/**
|
|
165
180
|
* Clear the request-scoped query cache on every live connection at the start of
|
|
166
181
|
* each HTTP request, so request-scoped caching never serves rows across
|
|
@@ -535,7 +550,7 @@ export class Database {
|
|
|
535
550
|
db.adapter = null; // Don't use single-adapter path
|
|
536
551
|
db.adapterFactory = async () => wrapWithCache(await createAdapterFromUrl(url, username, password), { sharedCache });
|
|
537
552
|
db.dbType = parsed.type;
|
|
538
|
-
return db;
|
|
553
|
+
return exposeDb(db);
|
|
539
554
|
}
|
|
540
555
|
|
|
541
556
|
// Single-connection mode — wrap once and share the SAME wrapped adapter
|
|
@@ -545,7 +560,7 @@ export class Database {
|
|
|
545
560
|
const wrapped = setAdapter(adapter);
|
|
546
561
|
const db = new Database(wrapped);
|
|
547
562
|
db.dbType = parsed.type;
|
|
548
|
-
return db;
|
|
563
|
+
return exposeDb(db);
|
|
549
564
|
}
|
|
550
565
|
|
|
551
566
|
/**
|
|
@@ -1217,10 +1232,11 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1217
1232
|
if (pool > 0) {
|
|
1218
1233
|
// Pool-aware path — delegate to Database.create which manages
|
|
1219
1234
|
// round-robin adapter rotation and async-local-storage transaction pinning.
|
|
1220
|
-
|
|
1235
|
+
// Database.create already exposes the global; exposeDb here is idempotent.
|
|
1236
|
+
return exposeDb(await Database.create(url, resolvedUser, resolvedPassword, pool));
|
|
1221
1237
|
}
|
|
1222
1238
|
const adapter = await createAdapterFromUrl(url, resolvedUser, resolvedPassword);
|
|
1223
|
-
return new Database(setAdapter(adapter));
|
|
1239
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1224
1240
|
}
|
|
1225
1241
|
|
|
1226
1242
|
// Legacy config path — normalize "sqlserver" to "mssql"
|
|
@@ -1245,7 +1261,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1245
1261
|
case "sqlite": {
|
|
1246
1262
|
const { SQLiteAdapter } = await import("./adapters/sqlite.js");
|
|
1247
1263
|
const adapter = new SQLiteAdapter(config?.path ?? "./data/tina4.db");
|
|
1248
|
-
return new Database(setAdapter(adapter));
|
|
1264
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1249
1265
|
}
|
|
1250
1266
|
case "postgres": {
|
|
1251
1267
|
const { PostgresAdapter } = await import("./adapters/postgres.js");
|
|
@@ -1257,7 +1273,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1257
1273
|
database: config?.database,
|
|
1258
1274
|
});
|
|
1259
1275
|
await adapter.connect();
|
|
1260
|
-
return new Database(setAdapter(adapter));
|
|
1276
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1261
1277
|
}
|
|
1262
1278
|
case "mysql": {
|
|
1263
1279
|
const { MysqlAdapter } = await import("./adapters/mysql.js");
|
|
@@ -1269,7 +1285,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1269
1285
|
database: config?.database,
|
|
1270
1286
|
});
|
|
1271
1287
|
await adapter.connect();
|
|
1272
|
-
return new Database(setAdapter(adapter));
|
|
1288
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1273
1289
|
}
|
|
1274
1290
|
case "mssql": {
|
|
1275
1291
|
const { MssqlAdapter } = await import("./adapters/mssql.js");
|
|
@@ -1281,7 +1297,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1281
1297
|
database: config?.database,
|
|
1282
1298
|
});
|
|
1283
1299
|
await adapter.connect();
|
|
1284
|
-
return new Database(setAdapter(adapter));
|
|
1300
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1285
1301
|
}
|
|
1286
1302
|
case "firebird": {
|
|
1287
1303
|
const { FirebirdAdapter } = await import("./adapters/firebird.js");
|
|
@@ -1293,7 +1309,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1293
1309
|
database: config?.database,
|
|
1294
1310
|
});
|
|
1295
1311
|
await adapter.connect();
|
|
1296
|
-
return new Database(setAdapter(adapter));
|
|
1312
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1297
1313
|
}
|
|
1298
1314
|
case "mongodb": {
|
|
1299
1315
|
const { MongodbAdapter } = await import("./adapters/mongodb.js");
|
|
@@ -1306,14 +1322,14 @@ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
|
|
|
1306
1322
|
const connectionString = `mongodb://${creds}${host}:${port}/${database}`;
|
|
1307
1323
|
const adapter = new MongodbAdapter(connectionString);
|
|
1308
1324
|
await adapter.connect();
|
|
1309
|
-
return new Database(setAdapter(adapter));
|
|
1325
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1310
1326
|
}
|
|
1311
1327
|
case "odbc": {
|
|
1312
1328
|
const { OdbcAdapter } = await import("./adapters/odbc.js");
|
|
1313
1329
|
const connStr = config?.connectionString ?? config?.url?.replace(/^odbc:\/\/\//, "") ?? "";
|
|
1314
1330
|
const adapter = new OdbcAdapter({ connectionString: connStr });
|
|
1315
1331
|
await adapter.connect();
|
|
1316
|
-
return new Database(setAdapter(adapter));
|
|
1332
|
+
return exposeDb(new Database(setAdapter(adapter)));
|
|
1317
1333
|
}
|
|
1318
1334
|
default:
|
|
1319
1335
|
throw new Error(`Unknown database type: ${type}`);
|