tina4-nodejs 3.11.17 → 3.11.19
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 +1 -1
- package/package.json +1 -1
- package/packages/core/public/js/tina4-dev-admin.js +260 -111
- package/packages/core/public/js/tina4-dev-admin.min.js +260 -111
- package/packages/core/src/background.ts +114 -0
- package/packages/core/src/devAdmin.ts +119 -1
- package/packages/core/src/docs.ts +1231 -0
- package/packages/core/src/docsAutoDiscovery.ts +73 -0
- package/packages/core/src/index.ts +4 -0
- package/packages/core/src/mcp.ts +67 -0
- package/packages/core/src/server.ts +11 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Docs MCP discovery — writes `.tina4/mcp.json` so MCP-aware AI tools
|
|
3
|
+
* (Claude Code, Cursor, etc.) auto-discover this server's docs MCP endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent — only writes if content has changed.
|
|
6
|
+
*
|
|
7
|
+
* Spec: plan/v3/22-LIVE-API-RAG.md (Auto-discovery section)
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
interface McpDiscovery {
|
|
13
|
+
mcpServers: Record<string, { url: string; description: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TINA4_DIR = ".tina4";
|
|
17
|
+
const MCP_FILE = "mcp.json";
|
|
18
|
+
const GITIGNORE_LINE = ".tina4/";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write `.tina4/mcp.json` and (if `.git/` exists) append `.tina4/` to .gitignore.
|
|
22
|
+
*
|
|
23
|
+
* Returns true if the discovery file was written or updated.
|
|
24
|
+
*/
|
|
25
|
+
export function writeMcpDiscovery(projectRoot: string, port: number): boolean {
|
|
26
|
+
const root = path.resolve(projectRoot);
|
|
27
|
+
const tinaDir = path.join(root, TINA4_DIR);
|
|
28
|
+
const mcpPath = path.join(tinaDir, MCP_FILE);
|
|
29
|
+
|
|
30
|
+
const portStr = String(
|
|
31
|
+
port || Number(process.env.TINA4_PORT) || Number(process.env.PORT) || 7148,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const desired: McpDiscovery = {
|
|
35
|
+
mcpServers: {
|
|
36
|
+
"tina4-live-docs": {
|
|
37
|
+
url: `http://localhost:${portStr}/__dev/api/mcp`,
|
|
38
|
+
description: "Live API docs for this Tina4 project (framework + user code)",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Idempotent write — compare before overwriting.
|
|
44
|
+
let existing: string | null = null;
|
|
45
|
+
if (fs.existsSync(mcpPath)) {
|
|
46
|
+
try { existing = fs.readFileSync(mcpPath, "utf-8"); } catch { /* fall through */ }
|
|
47
|
+
}
|
|
48
|
+
const desiredJson = JSON.stringify(desired, null, 2) + "\n";
|
|
49
|
+
let wrote = false;
|
|
50
|
+
if (existing !== desiredJson) {
|
|
51
|
+
fs.mkdirSync(tinaDir, { recursive: true });
|
|
52
|
+
fs.writeFileSync(mcpPath, desiredJson, "utf-8");
|
|
53
|
+
wrote = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Only append .gitignore entry inside a real git repo.
|
|
57
|
+
const gitDir = path.join(root, ".git");
|
|
58
|
+
if (fs.existsSync(gitDir)) {
|
|
59
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
60
|
+
let contents = "";
|
|
61
|
+
if (fs.existsSync(gitignorePath)) {
|
|
62
|
+
try { contents = fs.readFileSync(gitignorePath, "utf-8"); } catch { /* leave empty */ }
|
|
63
|
+
}
|
|
64
|
+
const lines = contents.split(/\r?\n/);
|
|
65
|
+
const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
|
|
66
|
+
if (!already) {
|
|
67
|
+
const sep = contents.endsWith("\n") || contents === "" ? "" : "\n";
|
|
68
|
+
fs.writeFileSync(gitignorePath, `${contents}${sep}${GITIGNORE_LINE}\n`, "utf-8");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return wrote;
|
|
73
|
+
}
|
|
@@ -13,6 +13,7 @@ export type {
|
|
|
13
13
|
} from "./types.js";
|
|
14
14
|
|
|
15
15
|
export { startServer, resolvePortAndHost, handle, start, stop } from "./server.js";
|
|
16
|
+
export { background, stopAllBackgroundTasks, backgroundTaskCount } from "./background.js";
|
|
16
17
|
export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares } from "./router.js";
|
|
17
18
|
export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
|
|
18
19
|
export type { RouteInfo } from "./router.js";
|
|
@@ -115,3 +116,6 @@ export { Plan } from "./plan.js";
|
|
|
115
116
|
export type { PlanStep, ParsedPlan, PlanSummary, ExecutionSummary, CurrentPlan } from "./plan.js";
|
|
116
117
|
export { ProjectIndex } from "./projectIndex.js";
|
|
117
118
|
export type { FileEntry, FileRoute } from "./projectIndex.js";
|
|
119
|
+
export { Docs } from "./docs.js";
|
|
120
|
+
export type { DocsHit, ClassSpec, MethodSpec, IndexEntry, DriftHit } from "./docs.js";
|
|
121
|
+
export { writeMcpDiscovery } from "./docsAutoDiscovery.js";
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -1332,6 +1332,73 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1332
1332
|
"List this project's declared Node.js dependencies",
|
|
1333
1333
|
schemaFromParams([]),
|
|
1334
1334
|
);
|
|
1335
|
+
|
|
1336
|
+
// ── Live API RAG (Docs) — plan/v3/22-LIVE-API-RAG.md ──────────
|
|
1337
|
+
|
|
1338
|
+
server.registerTool(
|
|
1339
|
+
"api_search",
|
|
1340
|
+
(args) => {
|
|
1341
|
+
try {
|
|
1342
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1343
|
+
const { Docs } = require("./docs.js") as typeof import("./docs.js");
|
|
1344
|
+
return Docs.mcpSearch(
|
|
1345
|
+
(args.query as string) || "",
|
|
1346
|
+
parseInt(String(args.k ?? 5), 10) || 5,
|
|
1347
|
+
undefined,
|
|
1348
|
+
(args.source as string) || "all",
|
|
1349
|
+
Boolean(args.include_private),
|
|
1350
|
+
);
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
return { error: (e as Error).message };
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
"Search the live API index (framework + user code) for matching classes/methods",
|
|
1356
|
+
schemaFromParams([
|
|
1357
|
+
{ name: "query", type: "string" },
|
|
1358
|
+
{ name: "k", type: "integer", default: 5 },
|
|
1359
|
+
{ name: "source", type: "string", default: "all" },
|
|
1360
|
+
{ name: "include_private", type: "boolean", default: false },
|
|
1361
|
+
]),
|
|
1362
|
+
);
|
|
1363
|
+
|
|
1364
|
+
server.registerTool(
|
|
1365
|
+
"api_class",
|
|
1366
|
+
(args) => {
|
|
1367
|
+
try {
|
|
1368
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1369
|
+
const { Docs } = require("./docs.js") as typeof import("./docs.js");
|
|
1370
|
+
const spec = Docs.mcpClass((args.name as string) || "");
|
|
1371
|
+
return spec ?? { error: `class not found: ${args.name}` };
|
|
1372
|
+
} catch (e) {
|
|
1373
|
+
return { error: (e as Error).message };
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
"Return the full class spec (methods + properties) for a single class FQN",
|
|
1377
|
+
schemaFromParams([{ name: "name", type: "string" }]),
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
server.registerTool(
|
|
1381
|
+
"api_method",
|
|
1382
|
+
(args) => {
|
|
1383
|
+
try {
|
|
1384
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1385
|
+
const { Docs } = require("./docs.js") as typeof import("./docs.js");
|
|
1386
|
+
// PHP names the param `class`, Python names it `class_` — Node.js MCP
|
|
1387
|
+
// accepts the raw `class` field from the JSON-RPC payload.
|
|
1388
|
+
const cls = (args.class as string) || (args.class_name as string) || "";
|
|
1389
|
+
const name = (args.name as string) || "";
|
|
1390
|
+
const spec = Docs.mcpMethod(cls, name);
|
|
1391
|
+
return spec ?? { error: `method not found: ${cls}.${name}` };
|
|
1392
|
+
} catch (e) {
|
|
1393
|
+
return { error: (e as Error).message };
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
"Return the full spec for a single method (signature, file, line, visibility)",
|
|
1397
|
+
schemaFromParams([
|
|
1398
|
+
{ name: "class", type: "string" },
|
|
1399
|
+
{ name: "name", type: "string" },
|
|
1400
|
+
]),
|
|
1401
|
+
);
|
|
1335
1402
|
}
|
|
1336
1403
|
|
|
1337
1404
|
/** Alias for registerDevTools — parity with PHP/Ruby/Python. */
|
|
@@ -20,6 +20,7 @@ import { rateLimiter } from "./rateLimiter.js";
|
|
|
20
20
|
import { Log } from "./logger.js";
|
|
21
21
|
import { DevAdmin, RequestInspector } from "./devAdmin.js";
|
|
22
22
|
import { I18n } from "./i18n.js";
|
|
23
|
+
import { stopAllBackgroundTasks } from "./background.js";
|
|
23
24
|
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
26
|
const __dirname = dirname(__filename);
|
|
@@ -549,6 +550,7 @@ ${reset}
|
|
|
549
550
|
// Return a handle that kills all workers
|
|
550
551
|
return {
|
|
551
552
|
close: () => {
|
|
553
|
+
stopAllBackgroundTasks();
|
|
552
554
|
for (const id in cluster.workers) {
|
|
553
555
|
cluster.workers[id]?.kill();
|
|
554
556
|
}
|
|
@@ -713,6 +715,13 @@ ${reset}
|
|
|
713
715
|
if (DevAdmin.isEnabled()) {
|
|
714
716
|
DevAdmin.register(router);
|
|
715
717
|
console.log(` Dev dashboard at \x1b[36mhttp://localhost:${port}/__dev\x1b[0m`);
|
|
718
|
+
// Live Docs MCP discovery — write .tina4/mcp.json so AI tools find this server.
|
|
719
|
+
try {
|
|
720
|
+
const { writeMcpDiscovery } = await import("./docsAutoDiscovery.js");
|
|
721
|
+
writeMcpDiscovery(process.cwd(), Number(port));
|
|
722
|
+
} catch (e) {
|
|
723
|
+
console.log(` (mcp discovery skipped: ${(e as Error).message})`);
|
|
724
|
+
}
|
|
716
725
|
}
|
|
717
726
|
|
|
718
727
|
async function dispatch(rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void> {
|
|
@@ -1075,6 +1084,8 @@ ${reset}
|
|
|
1075
1084
|
}
|
|
1076
1085
|
resolvePromise({
|
|
1077
1086
|
close: () => {
|
|
1087
|
+
// Clear any registered background timers so graceful shutdown actually exits.
|
|
1088
|
+
stopAllBackgroundTasks();
|
|
1078
1089
|
if (aiServer) aiServer.close();
|
|
1079
1090
|
server.close();
|
|
1080
1091
|
// Close database if ORM was initialized
|