tina4-nodejs 3.13.34 → 3.13.36
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 +11 -8
- package/package.json +1 -1
- package/packages/core/public/js/tina4-dev-admin.js +437 -759
- package/packages/core/public/js/tina4-dev-admin.min.js +437 -759
- package/packages/core/src/devAdmin.ts +305 -28
- package/packages/core/src/index.ts +2 -1
- package/packages/core/src/mcp.test.ts +25 -25
- package/packages/core/src/mcp.ts +112 -47
- package/packages/core/src/routeDiscovery.ts +23 -5
- package/packages/core/src/server.ts +46 -1
- package/packages/core/src/websocket.ts +139 -0
- package/packages/orm/src/database.ts +27 -11
|
@@ -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
|
/**
|
|
@@ -648,14 +665,37 @@ const handleReload: RouteHandler = async (req, res) => {
|
|
|
648
665
|
const { rediscoverRoutes } = await import("./routeDiscovery.js");
|
|
649
666
|
const newRoutes = await rediscoverRoutes();
|
|
650
667
|
if (newRoutes.length > 0) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
668
|
+
// Add to the LIVE server router — startServer() builds a fresh Router and
|
|
669
|
+
// exposes it as globalThis.__tina4_router; that's the instance dispatch
|
|
670
|
+
// matches against. Adding to defaultRouter would land the re-imported
|
|
671
|
+
// handler in a table nobody serves from, so the stale route keeps winning
|
|
672
|
+
// (an edited route would never hot-reload). addRoute() replaces by pattern,
|
|
673
|
+
// so the fresh handler overwrites the old one in place. Fall back to
|
|
674
|
+
// defaultRouter when no server is running (e.g. unit tests).
|
|
675
|
+
const liveRouter = (globalThis as any).__tina4_router;
|
|
676
|
+
const target = liveRouter ?? (await import("./router.js")).defaultRouter;
|
|
677
|
+
for (const route of newRoutes) target.addRoute(route);
|
|
678
|
+
console.log(` Re-discovered ${newRoutes.length} route(s) on reload`);
|
|
654
679
|
}
|
|
655
680
|
} catch (err) {
|
|
656
681
|
console.error(` Re-discover on reload failed:`, err);
|
|
657
682
|
}
|
|
658
683
|
|
|
684
|
+
// WebSocket-primary reload: push an instant message to every browser
|
|
685
|
+
// connected on /__dev_reload. The toolbar client (and the dev-admin
|
|
686
|
+
// dashboard) act on this immediately — the mtime poll is only a fallback for
|
|
687
|
+
// when the socket is down. CSS changes swap stylesheets; everything else
|
|
688
|
+
// triggers a full page reload, so we normalise the wire `type` to
|
|
689
|
+
// "css"/"reload". The HTTP response still echoes the caller's original type.
|
|
690
|
+
// Wrapped so a broadcast failure (or zero clients) never 500s the endpoint.
|
|
691
|
+
const wsType = reloadType === "css" ? "css" : "reload";
|
|
692
|
+
try {
|
|
693
|
+
const { devReloadWs } = await import("./websocket.js");
|
|
694
|
+
devReloadWs.broadcast(JSON.stringify({ type: wsType, file: _reloadFile, mtime: _reloadMtime }));
|
|
695
|
+
} catch (err) {
|
|
696
|
+
console.error(` Dev-reload WebSocket broadcast failed:`, err);
|
|
697
|
+
}
|
|
698
|
+
|
|
659
699
|
res.json({ ok: true, type: reloadType });
|
|
660
700
|
};
|
|
661
701
|
|
|
@@ -1717,35 +1757,151 @@ const handleExecute: RouteHandler = async (req, res) => {
|
|
|
1717
1757
|
}
|
|
1718
1758
|
};
|
|
1719
1759
|
|
|
1720
|
-
|
|
1760
|
+
// --- file-browser noise filter + git decoration (mirrors PHP/Python dev-admin) ---
|
|
1761
|
+
const DEV_FILES_IGNORED = new Set([
|
|
1762
|
+
"__pycache__", "node_modules", "vendor", ".git",
|
|
1763
|
+
"venv", ".venv", "dist", "target", ".tina4",
|
|
1764
|
+
]);
|
|
1765
|
+
|
|
1766
|
+
// Hidden dot-entries are filtered too, except the env files.
|
|
1767
|
+
function devFilesHidden(name: string): boolean {
|
|
1768
|
+
if (DEV_FILES_IGNORED.has(name)) return true;
|
|
1769
|
+
return name.startsWith(".") && name !== ".env" && name !== ".env.example";
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// Same 4-status mapping Python/PHP use for a porcelain code.
|
|
1773
|
+
function devGitStatusLabel(code: string): string {
|
|
1774
|
+
if (code === "??") return "untracked";
|
|
1775
|
+
if (code.includes("M")) return "modified";
|
|
1776
|
+
if (code.includes("A")) return "added";
|
|
1777
|
+
if (code.includes("D")) return "deleted";
|
|
1778
|
+
return "clean";
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Branch + porcelain status map for the file browser, mirroring PHP's
|
|
1783
|
+
* devAdminGit* helpers 1:1. Paths git reports are relative to the repo
|
|
1784
|
+
* root (always forward-slash); the project root may sit inside a larger
|
|
1785
|
+
* repo (monorepo), so the toplevel is returned too for rebasing. Degrades
|
|
1786
|
+
* to empty (every entry "clean") on any error / no git.
|
|
1787
|
+
*/
|
|
1788
|
+
async function devGitInfo(root: string): Promise<{ branch: string; gitRoot: string | null; status: Map<string, string> }> {
|
|
1789
|
+
const empty = { branch: "", gitRoot: null as string | null, status: new Map<string, string>() };
|
|
1790
|
+
try {
|
|
1791
|
+
const { execFileSync } = await import("node:child_process");
|
|
1792
|
+
const git = (args: string[]): string | null => {
|
|
1793
|
+
try {
|
|
1794
|
+
return execFileSync("git", args, { cwd: root, timeout: 3000, encoding: "utf-8" }).toString();
|
|
1795
|
+
} catch {
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
const inside = git(["rev-parse", "--is-inside-work-tree"]);
|
|
1800
|
+
if (!inside || inside.trim() !== "true") return empty;
|
|
1801
|
+
const branch = (git(["rev-parse", "--abbrev-ref", "HEAD"]) ?? "").trim();
|
|
1802
|
+
const topRaw = git(["rev-parse", "--show-toplevel"]);
|
|
1803
|
+
const gitRoot = topRaw && topRaw.trim() !== ""
|
|
1804
|
+
? topRaw.trim().replace(/\\/g, "/").replace(/\/+$/, "")
|
|
1805
|
+
: null;
|
|
1806
|
+
const status = new Map<string, string>();
|
|
1807
|
+
const porcelain = git(["status", "--porcelain", "-uall"]);
|
|
1808
|
+
if (porcelain) {
|
|
1809
|
+
for (const line of porcelain.split(/\r?\n/)) {
|
|
1810
|
+
if (line.length < 4) continue;
|
|
1811
|
+
const code = line.slice(0, 2).trim();
|
|
1812
|
+
let p = line.slice(3).trim();
|
|
1813
|
+
const arrow = p.indexOf(" -> "); // rename/copy — keep destination
|
|
1814
|
+
if (arrow !== -1) p = p.slice(arrow + 4);
|
|
1815
|
+
if (p === "") continue;
|
|
1816
|
+
status.set(p, code);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return { branch, gitRoot, status };
|
|
1820
|
+
} catch {
|
|
1821
|
+
return empty;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const handleFiles: RouteHandler = async (req, res) => {
|
|
1826
|
+
// Response shape matches tina4-python / tina4-php 1:1 so the dev-admin SPA
|
|
1827
|
+
// works against every framework with no branching: each entry carries
|
|
1828
|
+
// `is_dir`, `has_children`, `git_status` and `size`; the payload carries the
|
|
1829
|
+
// git `branch`. Noise dirs + hidden dot-files (except .env/.env.example) are
|
|
1830
|
+
// filtered out.
|
|
1721
1831
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1722
1832
|
const rel = url.searchParams.get("path") ?? ".";
|
|
1723
1833
|
const root = resolve(process.cwd());
|
|
1724
1834
|
const target = safeJoin(root, rel);
|
|
1725
|
-
|
|
1726
|
-
|
|
1835
|
+
const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
|
|
1836
|
+
|
|
1837
|
+
// Missing/invalid paths return an empty-but-valid shape (not 404): the SPA
|
|
1838
|
+
// restores expanded-folder state from localStorage, and folders that don't
|
|
1839
|
+
// exist in this harness would otherwise spam the console with red 404s.
|
|
1840
|
+
if (!target || !existsSync(target) || !statSync(target).isDirectory()) {
|
|
1841
|
+
res.json({ path: rel, branch, entries: [], error: "not a directory" });
|
|
1727
1842
|
return;
|
|
1728
1843
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1844
|
+
|
|
1845
|
+
// Rebase entry paths onto the git repo root when the project sits inside a
|
|
1846
|
+
// larger repo. Everything runs in forward-slash form.
|
|
1847
|
+
const rootFwd = root.replace(/\\/g, "/");
|
|
1848
|
+
let cwdInGit = "";
|
|
1849
|
+
if (gitRoot && gitRoot !== rootFwd && rootFwd.startsWith(gitRoot)) {
|
|
1850
|
+
cwdInGit = rootFwd.slice(gitRoot.length).replace(/^\/+/, "");
|
|
1851
|
+
if (cwdInGit !== "") cwdInGit += "/";
|
|
1733
1852
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1853
|
+
|
|
1854
|
+
const entries: Array<Record<string, unknown>> = [];
|
|
1855
|
+
for (const name of readdirSync(target).sort()) { // alphabetical; no re-sort by type
|
|
1856
|
+
if (devFilesHidden(name)) continue;
|
|
1857
|
+
const full = join(target, name);
|
|
1858
|
+
const entryRel = relative(root, full).replace(/\\/g, "/");
|
|
1859
|
+
|
|
1860
|
+
let isDir = false;
|
|
1861
|
+
let size: number | null = null;
|
|
1862
|
+
try {
|
|
1863
|
+
const st = statSync(full);
|
|
1864
|
+
isDir = st.isDirectory();
|
|
1865
|
+
if (!isDir) size = st.size;
|
|
1866
|
+
} catch { /* unreadable entry */ }
|
|
1867
|
+
|
|
1868
|
+
// git status for this entry (same mapping PHP/Python use)
|
|
1869
|
+
const gitPath = cwdInGit + entryRel;
|
|
1870
|
+
let gitLabel = "clean";
|
|
1871
|
+
const code = gitStatus.get(gitPath);
|
|
1872
|
+
if (code !== undefined) {
|
|
1873
|
+
gitLabel = devGitStatusLabel(code);
|
|
1874
|
+
} else if (isDir) {
|
|
1875
|
+
const prefix = gitPath + "/"; // propagate dirty status from any child
|
|
1876
|
+
for (const [gf, gc] of gitStatus) {
|
|
1877
|
+
if (gf.startsWith(prefix)) { gitLabel = gc === "??" ? "untracked" : "modified"; break; }
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// has_children: does the dir contain anything visible?
|
|
1882
|
+
let hasChildren: boolean | null = null;
|
|
1883
|
+
if (isDir) {
|
|
1884
|
+
hasChildren = false;
|
|
1885
|
+
try {
|
|
1886
|
+
for (const c of readdirSync(full)) {
|
|
1887
|
+
if (devFilesHidden(c)) continue;
|
|
1888
|
+
hasChildren = true;
|
|
1889
|
+
break;
|
|
1890
|
+
}
|
|
1891
|
+
} catch { /* ignore */ }
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
entries.push({
|
|
1895
|
+
name,
|
|
1896
|
+
path: entryRel,
|
|
1897
|
+
is_dir: isDir,
|
|
1898
|
+
has_children: hasChildren,
|
|
1899
|
+
git_status: gitLabel,
|
|
1900
|
+
size,
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
res.json({ path: relative(root, target).replace(/\\/g, "/") || ".", branch, entries });
|
|
1749
1905
|
};
|
|
1750
1906
|
|
|
1751
1907
|
const handleFileRead: RouteHandler = (req, res) => {
|
|
@@ -1931,7 +2087,11 @@ const handleGitStatus: RouteHandler = async (_req, res) => {
|
|
|
1931
2087
|
|
|
1932
2088
|
const handleMcpTools: RouteHandler = async (_req, res) => {
|
|
1933
2089
|
try {
|
|
1934
|
-
|
|
2090
|
+
// Ensure the default /__dev/mcp server exists with its dev tools registered,
|
|
2091
|
+
// then enumerate every registered MCP server instance (app-defined servers
|
|
2092
|
+
// register themselves on construction too).
|
|
2093
|
+
const { McpServer, getDefaultDevServer } = await import("./mcp.js");
|
|
2094
|
+
getDefaultDevServer();
|
|
1935
2095
|
const instances = (McpServer as unknown as { _instances: Array<any> })._instances || [];
|
|
1936
2096
|
const tools: Array<{ server: string; name: string; description: string; inputSchema: unknown }> = [];
|
|
1937
2097
|
for (const s of instances) {
|
|
@@ -1950,7 +2110,8 @@ const handleMcpCall: RouteHandler = async (req, res) => {
|
|
|
1950
2110
|
const name = (body.name as string) || "";
|
|
1951
2111
|
const args = (body.arguments as Record<string, unknown>) || {};
|
|
1952
2112
|
try {
|
|
1953
|
-
const { McpServer } = await import("./mcp.js");
|
|
2113
|
+
const { McpServer, getDefaultDevServer } = await import("./mcp.js");
|
|
2114
|
+
getDefaultDevServer();
|
|
1954
2115
|
const instances = (McpServer as unknown as { _instances: Array<any> })._instances || [];
|
|
1955
2116
|
for (const s of instances) {
|
|
1956
2117
|
const tool = ((s as any)._tools as Map<string, any>).get(name);
|
|
@@ -1966,6 +2127,59 @@ const handleMcpCall: RouteHandler = async (req, res) => {
|
|
|
1966
2127
|
}
|
|
1967
2128
|
};
|
|
1968
2129
|
|
|
2130
|
+
/**
|
|
2131
|
+
* JSON-RPC message endpoint for real MCP clients.
|
|
2132
|
+
*
|
|
2133
|
+
* Mounted at POST /__dev/mcp and POST /__dev/mcp/message. Forwards the request
|
|
2134
|
+
* body to the default dev MCP server's handleMessage() and returns the JSON-RPC
|
|
2135
|
+
* response. Notifications / id-less requests yield an empty 204. Mirrors the
|
|
2136
|
+
* Python v3 fix. The /__dev path is always public (auth-bypassed), so MCP
|
|
2137
|
+
* clients connect without a token.
|
|
2138
|
+
*/
|
|
2139
|
+
const handleMcpMessage: RouteHandler = async (req, res) => {
|
|
2140
|
+
try {
|
|
2141
|
+
const { getDefaultDevServer } = await import("./mcp.js");
|
|
2142
|
+
const server = getDefaultDevServer();
|
|
2143
|
+
const body = req.body;
|
|
2144
|
+
let raw: string | Record<string, unknown>;
|
|
2145
|
+
if (typeof body === "object" && body !== null) {
|
|
2146
|
+
raw = body as Record<string, unknown>;
|
|
2147
|
+
} else {
|
|
2148
|
+
raw = typeof body === "string" ? body : String(body ?? "");
|
|
2149
|
+
}
|
|
2150
|
+
const result = await server.handleMessage(raw);
|
|
2151
|
+
if (!result) {
|
|
2152
|
+
// Notification / no id — nothing to return.
|
|
2153
|
+
res.send("", 204);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
res.json(JSON.parse(result));
|
|
2157
|
+
} catch (e) {
|
|
2158
|
+
res.json({ error: (e as Error).message }, 500);
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* SSE handshake endpoint for real MCP clients.
|
|
2164
|
+
*
|
|
2165
|
+
* Mounted at GET /__dev/mcp/sse. Announces the JSON-RPC message endpoint via an
|
|
2166
|
+
* `endpoint` event, exactly like the canonical McpServer.registerRoutes() and
|
|
2167
|
+
* the Python v3 fix. Content-Type text/event-stream, status 200.
|
|
2168
|
+
*/
|
|
2169
|
+
const handleMcpSse: RouteHandler = async (req, res) => {
|
|
2170
|
+
// req.path is the path only (no query); turn /__dev/mcp/sse into the message
|
|
2171
|
+
// endpoint /__dev/mcp/message that the client should POST to.
|
|
2172
|
+
const reqPath = req.path || "/__dev/mcp/sse";
|
|
2173
|
+
const endpointUrl = reqPath.replace(/\/sse$/, "/message");
|
|
2174
|
+
const sseData = `event: endpoint\ndata: ${endpointUrl}\n\n`;
|
|
2175
|
+
res.raw.writeHead(200, {
|
|
2176
|
+
"Content-Type": "text/event-stream",
|
|
2177
|
+
"Cache-Control": "no-cache",
|
|
2178
|
+
Connection: "keep-alive",
|
|
2179
|
+
});
|
|
2180
|
+
res.raw.end(sseData);
|
|
2181
|
+
};
|
|
2182
|
+
|
|
1969
2183
|
const handleScaffoldList: RouteHandler = (_req, res) => {
|
|
1970
2184
|
res.json({
|
|
1971
2185
|
scaffolds: [
|
|
@@ -2322,5 +2536,68 @@ function tina4VersionModal(){
|
|
|
2322
2536
|
el.style.color='#f38ba8';
|
|
2323
2537
|
});
|
|
2324
2538
|
}
|
|
2539
|
+
</script>
|
|
2540
|
+
<script>
|
|
2541
|
+
(function(){
|
|
2542
|
+
// WebSocket-primary dev reloader. The running server re-imports changed
|
|
2543
|
+
// src/ routes in-process and pushes a {type,file,mtime} message over
|
|
2544
|
+
// /__dev_reload — no respawn, instant refresh. The mtime poll below is a
|
|
2545
|
+
// FALLBACK only, started when the socket is down and stopped on connect.
|
|
2546
|
+
var _t4_css_exts=['.css','.scss'],_t4_debounce=null;
|
|
2547
|
+
var _t4_interval=3000;
|
|
2548
|
+
var _t4_ws=null,_t4_poll_timer=null,_t4_mtime=null;
|
|
2549
|
+
function _t4_apply(d){
|
|
2550
|
+
d=d||{};
|
|
2551
|
+
var f=d.file||'',t=d.type||'';
|
|
2552
|
+
var isCss=t==='css'||_t4_css_exts.some(function(e){return f.endsWith(e)});
|
|
2553
|
+
if(isCss){
|
|
2554
|
+
var links=document.querySelectorAll('link[rel="stylesheet"]');
|
|
2555
|
+
links.forEach(function(l){
|
|
2556
|
+
var href=l.getAttribute('href');
|
|
2557
|
+
if(href){l.setAttribute('href',href.split('?')[0]+'?_t4='+(d.mtime||Date.now()))}
|
|
2558
|
+
});
|
|
2559
|
+
}else{
|
|
2560
|
+
location.reload();
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
function _t4_poll(){
|
|
2564
|
+
fetch('/__dev/api/mtime').then(function(r){return r.json()}).then(function(d){
|
|
2565
|
+
// Sentinel: first poll only records the baseline. Use !== (not >) so
|
|
2566
|
+
// the first change after load is not swallowed and a counter reset on
|
|
2567
|
+
// server restart still triggers a reload.
|
|
2568
|
+
if(_t4_mtime===null){_t4_mtime=d.mtime;return;}
|
|
2569
|
+
if(d.mtime!==_t4_mtime){
|
|
2570
|
+
_t4_mtime=d.mtime;
|
|
2571
|
+
if(_t4_debounce)clearTimeout(_t4_debounce);
|
|
2572
|
+
_t4_debounce=setTimeout(function(){_t4_apply(d);},500);
|
|
2573
|
+
}
|
|
2574
|
+
}).catch(function(){});
|
|
2575
|
+
}
|
|
2576
|
+
function _t4_startPoll(){
|
|
2577
|
+
if(_t4_poll_timer)return;
|
|
2578
|
+
_t4_mtime=null;
|
|
2579
|
+
_t4_poll_timer=setInterval(_t4_poll,_t4_interval);
|
|
2580
|
+
}
|
|
2581
|
+
function _t4_stopPoll(){
|
|
2582
|
+
if(_t4_poll_timer){clearInterval(_t4_poll_timer);_t4_poll_timer=null;}
|
|
2583
|
+
}
|
|
2584
|
+
function _t4_connect(){
|
|
2585
|
+
var url=(location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/__dev_reload';
|
|
2586
|
+
try{_t4_ws=new WebSocket(url);}catch(_){_t4_startPoll();return;}
|
|
2587
|
+
_t4_ws.addEventListener('open',function(){_t4_stopPoll();});
|
|
2588
|
+
_t4_ws.addEventListener('message',function(ev){
|
|
2589
|
+
var d=null;
|
|
2590
|
+
try{d=typeof ev.data==='string'?JSON.parse(ev.data):null;}catch(_){}
|
|
2591
|
+
if(!d)return;
|
|
2592
|
+
if(d.type==='reload'||d.type==='change'||d.type==='css'){
|
|
2593
|
+
if(_t4_debounce)clearTimeout(_t4_debounce);
|
|
2594
|
+
_t4_debounce=setTimeout(function(){_t4_apply(d);},150);
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
_t4_ws.addEventListener('close',function(){_t4_ws=null;_t4_startPoll();setTimeout(_t4_connect,2000);});
|
|
2598
|
+
_t4_ws.addEventListener('error',function(){try{_t4_ws&&_t4_ws.close();}catch(_){}});
|
|
2599
|
+
}
|
|
2600
|
+
_t4_connect();
|
|
2601
|
+
})();
|
|
2325
2602
|
</script>`;
|
|
2326
2603
|
}
|
|
@@ -61,6 +61,7 @@ export { GraphQL, ParseError, graphqlEndpoint, graphqlAutoSchemaEnabled } from "
|
|
|
61
61
|
export type { GraphQLField, ResolverFn, GraphQLResult } from "./graphql.js";
|
|
62
62
|
export {
|
|
63
63
|
WebSocketServer,
|
|
64
|
+
devReloadWs,
|
|
64
65
|
computeAcceptKey, parseUpgradeHeaders, buildFrame, parseFrame,
|
|
65
66
|
OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG,
|
|
66
67
|
CLOSE_NORMAL, CLOSE_PROTOCOL_ERROR,
|
|
@@ -121,7 +122,7 @@ export type { WebSocketConnection } from "./websocketConnection.js";
|
|
|
121
122
|
export { RedisBackplane, NATSBackplane, createBackplane } from "./websocketBackplane.js";
|
|
122
123
|
export type { WebSocketBackplane } from "./websocketBackplane.js";
|
|
123
124
|
export {
|
|
124
|
-
McpServer, mcpTool, mcpResource, registerDevTools,
|
|
125
|
+
McpServer, mcpTool, mcpResource, registerDevTools, getDefaultDevServer,
|
|
125
126
|
encodeResponse, encodeError, encodeNotification, decodeRequest,
|
|
126
127
|
schemaFromParams, isLocalhost, mcpEnabled, mcpPort,
|
|
127
128
|
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
|
});
|