tina4-nodejs 3.10.31 → 3.10.34
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
|
@@ -20,7 +20,7 @@ import { isTruthy } from "./dotenv.js";
|
|
|
20
20
|
|
|
21
21
|
const cpuCount = osCpus().length;
|
|
22
22
|
|
|
23
|
-
const TINA4_VERSION = "3.10.
|
|
23
|
+
const TINA4_VERSION = "3.10.34";
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
// Types
|
|
@@ -2009,7 +2009,17 @@ function renderToolbarHtml(ctx: {
|
|
|
2009
2009
|
}): string {
|
|
2010
2010
|
const nodeVersion = process.version;
|
|
2011
2011
|
return `<div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
|
|
2012
|
-
<span style="color:#2e7d32;font-weight:bold;">Tina4 v${ctx.version}</span>
|
|
2012
|
+
<span id="tina4-ver-btn" style="color:#2e7d32;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v${ctx.version}</span>
|
|
2013
|
+
<div id="tina4-ver-modal" style="display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #2e7d32;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace;font-size:13px;color:#cdd6f4;">
|
|
2014
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
|
2015
|
+
<strong style="color:#89b4fa;">Version Info</strong>
|
|
2016
|
+
<span onclick="document.getElementById('tina4-ver-modal').style.display='none'" style="cursor:pointer;color:#888;">×</span>
|
|
2017
|
+
</div>
|
|
2018
|
+
<div id="tina4-ver-body" style="line-height:1.8;">
|
|
2019
|
+
<div>Current: <strong style="color:#a6e3a1;">v${ctx.version}</strong></div>
|
|
2020
|
+
<div id="tina4-ver-latest" style="color:#888;">Checking for updates...</div>
|
|
2021
|
+
</div>
|
|
2022
|
+
</div>
|
|
2013
2023
|
<span style="color:#4caf50;">${ctx.method}</span>
|
|
2014
2024
|
<span>${ctx.path}</span>
|
|
2015
2025
|
<span style="color:#666;">→ ${ctx.matchedPattern}</span>
|
|
@@ -2018,5 +2028,50 @@ function renderToolbarHtml(ctx: {
|
|
|
2018
2028
|
<span style="color:#888;">Node.js ${nodeVersion}</span>
|
|
2019
2029
|
<a href="#" onclick="(function(e){e.preventDefault();var p=document.getElementById('tina4-dev-panel');if(p){p.style.display=p.style.display==='none'?'block':'none';return;}var c=document.createElement('div');c.id='tina4-dev-panel';c.style.cssText='position:fixed;bottom:2rem;right:1rem;width:min(90vw,1200px);height:min(80vh,700px);z-index:99998;transition:all 0.2s';var f=document.createElement('iframe');f.src='/__dev';f.style.cssText='width:100%;height:100%;border:1px solid #2e7d32;border-radius:0.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.5);background:#0f172a';c.appendChild(f);document.body.appendChild(c);})(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard ↗</a>
|
|
2020
2030
|
<span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">✕</span>
|
|
2021
|
-
</div
|
|
2031
|
+
</div>
|
|
2032
|
+
<script>
|
|
2033
|
+
function tina4VersionModal(){
|
|
2034
|
+
var m=document.getElementById('tina4-ver-modal');
|
|
2035
|
+
if(m.style.display==='block'){m.style.display='none';return;}
|
|
2036
|
+
m.style.display='block';
|
|
2037
|
+
var el=document.getElementById('tina4-ver-latest');
|
|
2038
|
+
el.innerHTML='Checking for updates...';
|
|
2039
|
+
el.style.color='#888';
|
|
2040
|
+
fetch('https://registry.npmjs.org/tina4-nodejs/latest')
|
|
2041
|
+
.then(function(r){return r.json()})
|
|
2042
|
+
.then(function(d){
|
|
2043
|
+
var latest=d.version;
|
|
2044
|
+
var current='${ctx.version}';
|
|
2045
|
+
if(latest===current){
|
|
2046
|
+
el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> — You are up to date!';
|
|
2047
|
+
el.style.color='#a6e3a1';
|
|
2048
|
+
}else{
|
|
2049
|
+
var cParts=current.split('.').map(Number);
|
|
2050
|
+
var lParts=latest.split('.').map(Number);
|
|
2051
|
+
var isNewer=false;
|
|
2052
|
+
for(var i=0;i<Math.max(cParts.length,lParts.length);i++){
|
|
2053
|
+
var c=cParts[i]||0,l=lParts[i]||0;
|
|
2054
|
+
if(l>c){isNewer=true;break;}
|
|
2055
|
+
if(l<c)break;
|
|
2056
|
+
}
|
|
2057
|
+
if(isNewer){
|
|
2058
|
+
var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
|
|
2059
|
+
el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
|
|
2060
|
+
if(breaking){
|
|
2061
|
+
el.innerHTML+='<div style="color:#f38ba8;margin-top:6px;">⚠ Major/minor version change — check the <a href="https://github.com/tina4stack/tina4-nodejs/releases" target="_blank" style="color:#89b4fa;">changelog</a> for breaking changes before upgrading.</div>';
|
|
2062
|
+
}else{
|
|
2063
|
+
el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">npm install tina4-nodejs@latest</code></div>';
|
|
2064
|
+
}
|
|
2065
|
+
}else{
|
|
2066
|
+
el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> — You are up to date!';
|
|
2067
|
+
el.style.color='#a6e3a1';
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
})
|
|
2071
|
+
.catch(function(){
|
|
2072
|
+
el.innerHTML='Could not check for updates (offline?)';
|
|
2073
|
+
el.style.color='#f38ba8';
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
</script>`;
|
|
2022
2077
|
}
|
|
@@ -100,3 +100,10 @@ export type { ValidationError } from "./validator.js";
|
|
|
100
100
|
export type { WebSocketConnection } from "./websocketConnection.js";
|
|
101
101
|
export { RedisBackplane, NATSBackplane, createBackplane } from "./websocketBackplane.js";
|
|
102
102
|
export type { WebSocketBackplane } from "./websocketBackplane.js";
|
|
103
|
+
export {
|
|
104
|
+
McpServer, mcpTool, mcpResource, registerDevTools,
|
|
105
|
+
encodeResponse, encodeError, encodeNotification, decodeRequest,
|
|
106
|
+
schemaFromParams, isLocalhost,
|
|
107
|
+
PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR,
|
|
108
|
+
} from "./mcp.js";
|
|
109
|
+
export type { JsonRpcMessage, McpToolDefinition, McpResourceDefinition, JsonSchema, McpToolParam } from "./mcp.js";
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the MCP (Model Context Protocol) implementation.
|
|
3
|
+
* Run with: npx tsx packages/core/src/mcp.test.ts
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
encodeResponse, encodeError, encodeNotification, decodeRequest,
|
|
7
|
+
McpServer, isLocalhost, schemaFromParams, registerDevTools,
|
|
8
|
+
PARSE_ERROR, METHOD_NOT_FOUND, INTERNAL_ERROR,
|
|
9
|
+
} from "./mcp.ts";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
|
|
14
|
+
let pass = 0;
|
|
15
|
+
let fail = 0;
|
|
16
|
+
|
|
17
|
+
function assert(name: string, condition: boolean, detail = "") {
|
|
18
|
+
if (condition) {
|
|
19
|
+
console.log(` \x1b[32mPASS\x1b[0m ${name}`);
|
|
20
|
+
pass++;
|
|
21
|
+
} else {
|
|
22
|
+
console.log(` \x1b[31mFAIL\x1b[0m ${name} ${detail}`);
|
|
23
|
+
fail++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertThrows(name: string, fn: () => void, match?: string) {
|
|
28
|
+
try {
|
|
29
|
+
fn();
|
|
30
|
+
console.log(` \x1b[31mFAIL\x1b[0m ${name} (no exception thrown)`);
|
|
31
|
+
fail++;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (match && !(e as Error).message.includes(match)) {
|
|
34
|
+
console.log(` \x1b[31mFAIL\x1b[0m ${name} (expected "${match}" in "${(e as Error).message}")`);
|
|
35
|
+
fail++;
|
|
36
|
+
} else {
|
|
37
|
+
console.log(` \x1b[32mPASS\x1b[0m ${name}`);
|
|
38
|
+
pass++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── JSON-RPC 2.0 Protocol ────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
console.log("\nJSON-RPC 2.0 Protocol");
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
const raw = encodeResponse(1, { tools: [] });
|
|
49
|
+
const msg = JSON.parse(raw);
|
|
50
|
+
assert("encodeResponse — jsonrpc", msg.jsonrpc === "2.0");
|
|
51
|
+
assert("encodeResponse — id", msg.id === 1);
|
|
52
|
+
assert("encodeResponse — result", JSON.stringify(msg.result) === JSON.stringify({ tools: [] }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
const raw = encodeError(2, METHOD_NOT_FOUND, "Not found");
|
|
57
|
+
const msg = JSON.parse(raw);
|
|
58
|
+
assert("encodeError — jsonrpc", msg.jsonrpc === "2.0");
|
|
59
|
+
assert("encodeError — id", msg.id === 2);
|
|
60
|
+
assert("encodeError — code", msg.error.code === -32601);
|
|
61
|
+
assert("encodeError — message", msg.error.message === "Not found");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
const raw = encodeError(3, INTERNAL_ERROR, "Fail", { detail: "extra" });
|
|
66
|
+
const msg = JSON.parse(raw);
|
|
67
|
+
assert("encodeError with data", msg.error.data?.detail === "extra");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
const { method, params, requestId } = decodeRequest(JSON.stringify({
|
|
72
|
+
jsonrpc: "2.0", id: 3, method: "tools/list", params: {},
|
|
73
|
+
}));
|
|
74
|
+
assert("decodeRequest — method", method === "tools/list");
|
|
75
|
+
assert("decodeRequest — params", JSON.stringify(params) === "{}");
|
|
76
|
+
assert("decodeRequest — id", requestId === 3);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
const { method, requestId } = decodeRequest({
|
|
81
|
+
jsonrpc: "2.0", method: "notifications/initialized",
|
|
82
|
+
});
|
|
83
|
+
assert("decodeRequest notification — method", method === "notifications/initialized");
|
|
84
|
+
assert("decodeRequest notification — id null", requestId === null);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
assertThrows("decodeRequest invalid JSON", () => decodeRequest("not json"), "Invalid JSON");
|
|
88
|
+
|
|
89
|
+
assertThrows("decodeRequest missing method", () => decodeRequest({ jsonrpc: "2.0", id: 1 } as any), "method");
|
|
90
|
+
|
|
91
|
+
assertThrows("decodeRequest missing jsonrpc", () => decodeRequest({ method: "test", id: 1 } as any), "jsonrpc");
|
|
92
|
+
|
|
93
|
+
// ── Notification Encoding ────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
console.log("\nNotification Encoding");
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
const raw = encodeNotification("test/event", { key: "value" });
|
|
99
|
+
const msg = JSON.parse(raw);
|
|
100
|
+
assert("encodeNotification — jsonrpc", msg.jsonrpc === "2.0");
|
|
101
|
+
assert("encodeNotification — method", msg.method === "test/event");
|
|
102
|
+
assert("encodeNotification — params", msg.params?.key === "value");
|
|
103
|
+
assert("encodeNotification — no id", msg.id === undefined);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
const raw = encodeNotification("test/simple");
|
|
108
|
+
const msg = JSON.parse(raw);
|
|
109
|
+
assert("encodeNotification without params", msg.params === undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── McpServer Core ───────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
console.log("\nMcpServer Core");
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
McpServer._instances = [];
|
|
118
|
+
const server = new McpServer("/test-mcp", "Test Server", "0.1.0");
|
|
119
|
+
const resp = JSON.parse(server.handleMessage({
|
|
120
|
+
jsonrpc: "2.0", id: 1, method: "initialize",
|
|
121
|
+
params: {
|
|
122
|
+
protocolVersion: "2024-11-05", capabilities: {},
|
|
123
|
+
clientInfo: { name: "test", version: "1.0" },
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
assert("initialize — protocolVersion", resp.result.protocolVersion === "2024-11-05");
|
|
127
|
+
assert("initialize — serverInfo.name", resp.result.serverInfo.name === "Test Server");
|
|
128
|
+
assert("initialize — capabilities.tools", resp.result.capabilities.tools !== undefined);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
{
|
|
132
|
+
McpServer._instances = [];
|
|
133
|
+
const server = new McpServer("/test-mcp", "Test Server");
|
|
134
|
+
const resp = JSON.parse(server.handleMessage({
|
|
135
|
+
jsonrpc: "2.0", id: 2, method: "ping", params: {},
|
|
136
|
+
}));
|
|
137
|
+
assert("ping — result empty", JSON.stringify(resp.result) === "{}");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
McpServer._instances = [];
|
|
142
|
+
const server = new McpServer("/test-mcp", "Test Server");
|
|
143
|
+
const resp = JSON.parse(server.handleMessage({
|
|
144
|
+
jsonrpc: "2.0", id: 3, method: "nonexistent", params: {},
|
|
145
|
+
}));
|
|
146
|
+
assert("method not found — code", resp.error.code === -32601);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
McpServer._instances = [];
|
|
151
|
+
const server = new McpServer("/test-mcp", "Test Server");
|
|
152
|
+
const resp = server.handleMessage({
|
|
153
|
+
jsonrpc: "2.0", method: "notifications/initialized",
|
|
154
|
+
});
|
|
155
|
+
assert("notification — empty response", resp === "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Tool Registration and Call ───────────────────────────────
|
|
159
|
+
|
|
160
|
+
console.log("\nTool Registration and Call");
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
McpServer._instances = [];
|
|
164
|
+
const server = new McpServer("/test-tools", "Tool Test");
|
|
165
|
+
server.registerTool(
|
|
166
|
+
"greet",
|
|
167
|
+
(args) => `Hello, ${args.name}!`,
|
|
168
|
+
"Greet someone",
|
|
169
|
+
schemaFromParams([{ name: "name", type: "string" }]),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const resp = JSON.parse(server.handleMessage({
|
|
173
|
+
jsonrpc: "2.0", id: 1, method: "tools/list", params: {},
|
|
174
|
+
}));
|
|
175
|
+
const tools = resp.result.tools;
|
|
176
|
+
assert("registerTool — count", tools.length === 1);
|
|
177
|
+
assert("registerTool — name", tools[0].name === "greet");
|
|
178
|
+
assert("registerTool — schema type", tools[0].inputSchema.properties.name.type === "string");
|
|
179
|
+
assert("registerTool — required", tools[0].inputSchema.required?.includes("name") === true);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
McpServer._instances = [];
|
|
184
|
+
const server = new McpServer("/test-tools", "Tool Test");
|
|
185
|
+
server.registerTool(
|
|
186
|
+
"add",
|
|
187
|
+
(args) => (args.a as number) + (args.b as number),
|
|
188
|
+
"Add two numbers",
|
|
189
|
+
schemaFromParams([{ name: "a", type: "integer" }, { name: "b", type: "integer" }]),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const resp = JSON.parse(server.handleMessage({
|
|
193
|
+
jsonrpc: "2.0", id: 2, method: "tools/call",
|
|
194
|
+
params: { name: "add", arguments: { a: 3, b: 5 } },
|
|
195
|
+
}));
|
|
196
|
+
const content = resp.result.content;
|
|
197
|
+
assert("callTool — content count", content.length === 1);
|
|
198
|
+
assert("callTool — type", content[0].type === "text");
|
|
199
|
+
assert("callTool — result contains 8", content[0].text.includes("8"));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
McpServer._instances = [];
|
|
204
|
+
const server = new McpServer("/test-tools", "Tool Test");
|
|
205
|
+
const resp = JSON.parse(server.handleMessage({
|
|
206
|
+
jsonrpc: "2.0", id: 3, method: "tools/call",
|
|
207
|
+
params: { name: "missing", arguments: {} },
|
|
208
|
+
}));
|
|
209
|
+
assert("unknown tool — error code", resp.error.code === -32603);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
{
|
|
213
|
+
McpServer._instances = [];
|
|
214
|
+
const server = new McpServer("/test-tools", "Tool Test");
|
|
215
|
+
server.registerTool("echo", (args) => args.msg as string, "Echo",
|
|
216
|
+
schemaFromParams([{ name: "msg", type: "string" }]));
|
|
217
|
+
|
|
218
|
+
const resp = JSON.parse(server.handleMessage({
|
|
219
|
+
jsonrpc: "2.0", id: 4, method: "tools/call",
|
|
220
|
+
params: { name: "echo", arguments: { msg: "hello" } },
|
|
221
|
+
}));
|
|
222
|
+
assert("tool string result", resp.result.content[0].text === "hello");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
McpServer._instances = [];
|
|
227
|
+
const server = new McpServer("/test-tools", "Tool Test");
|
|
228
|
+
server.registerTool("data", () => ({ a: 1, b: 2 }), "Return data");
|
|
229
|
+
|
|
230
|
+
const resp = JSON.parse(server.handleMessage({
|
|
231
|
+
jsonrpc: "2.0", id: 5, method: "tools/call",
|
|
232
|
+
params: { name: "data", arguments: {} },
|
|
233
|
+
}));
|
|
234
|
+
const data = JSON.parse(resp.result.content[0].text);
|
|
235
|
+
assert("tool object result — a", data.a === 1);
|
|
236
|
+
assert("tool object result — b", data.b === 2);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Schema from Params ───────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
console.log("\nSchema from Params");
|
|
242
|
+
|
|
243
|
+
{
|
|
244
|
+
const schema = schemaFromParams([
|
|
245
|
+
{ name: "name", type: "string" },
|
|
246
|
+
{ name: "count", type: "integer", default: 5 },
|
|
247
|
+
{ name: "active", type: "boolean", default: true },
|
|
248
|
+
]);
|
|
249
|
+
assert("schema — name type", schema.properties.name.type === "string");
|
|
250
|
+
assert("schema — count type", schema.properties.count.type === "integer");
|
|
251
|
+
assert("schema — count default", schema.properties.count.default === 5);
|
|
252
|
+
assert("schema — active type", schema.properties.active.type === "boolean");
|
|
253
|
+
assert("schema — required only name", JSON.stringify(schema.required) === '["name"]');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Resource Registration and Read ───────────────────────────
|
|
257
|
+
|
|
258
|
+
console.log("\nResource Registration and Read");
|
|
259
|
+
|
|
260
|
+
{
|
|
261
|
+
McpServer._instances = [];
|
|
262
|
+
const server = new McpServer("/test-resources", "Resource Test");
|
|
263
|
+
server.registerResource("app://tables", () => ["users", "products"], "Database tables");
|
|
264
|
+
|
|
265
|
+
const resp = JSON.parse(server.handleMessage({
|
|
266
|
+
jsonrpc: "2.0", id: 1, method: "resources/list", params: {},
|
|
267
|
+
}));
|
|
268
|
+
const resources = resp.result.resources;
|
|
269
|
+
assert("registerResource — count", resources.length === 1);
|
|
270
|
+
assert("registerResource — uri", resources[0].uri === "app://tables");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
{
|
|
274
|
+
McpServer._instances = [];
|
|
275
|
+
const server = new McpServer("/test-resources", "Resource Test");
|
|
276
|
+
server.registerResource("app://info", () => ({ version: "1.0", name: "Test App" }), "App info");
|
|
277
|
+
|
|
278
|
+
const resp = JSON.parse(server.handleMessage({
|
|
279
|
+
jsonrpc: "2.0", id: 2, method: "resources/read",
|
|
280
|
+
params: { uri: "app://info" },
|
|
281
|
+
}));
|
|
282
|
+
const contents = resp.result.contents;
|
|
283
|
+
assert("readResource — count", contents.length === 1);
|
|
284
|
+
const data = JSON.parse(contents[0].text);
|
|
285
|
+
assert("readResource — version", data.version === "1.0");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
{
|
|
289
|
+
McpServer._instances = [];
|
|
290
|
+
const server = new McpServer("/test-resources", "Resource Test");
|
|
291
|
+
const resp = JSON.parse(server.handleMessage({
|
|
292
|
+
jsonrpc: "2.0", id: 3, method: "resources/read",
|
|
293
|
+
params: { uri: "app://missing" },
|
|
294
|
+
}));
|
|
295
|
+
assert("unknown resource — error code", resp.error.code === -32603);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Localhost Detection ──────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
console.log("\nLocalhost Detection");
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
const oldHost = process.env.HOST_NAME;
|
|
304
|
+
|
|
305
|
+
process.env.HOST_NAME = "localhost:7148";
|
|
306
|
+
assert("isLocalhost — localhost", isLocalhost() === true);
|
|
307
|
+
|
|
308
|
+
process.env.HOST_NAME = "127.0.0.1:7148";
|
|
309
|
+
assert("isLocalhost — 127.0.0.1", isLocalhost() === true);
|
|
310
|
+
|
|
311
|
+
process.env.HOST_NAME = "0.0.0.0:7148";
|
|
312
|
+
assert("isLocalhost — 0.0.0.0", isLocalhost() === true);
|
|
313
|
+
|
|
314
|
+
process.env.HOST_NAME = "myserver.example.com:7148";
|
|
315
|
+
assert("isLocalhost — remote false", isLocalhost() === false);
|
|
316
|
+
|
|
317
|
+
// Restore
|
|
318
|
+
if (oldHost !== undefined) {
|
|
319
|
+
process.env.HOST_NAME = oldHost;
|
|
320
|
+
} else {
|
|
321
|
+
delete process.env.HOST_NAME;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── File Sandbox ─────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
console.log("\nFile Sandbox");
|
|
328
|
+
|
|
329
|
+
{
|
|
330
|
+
McpServer._instances = [];
|
|
331
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tina4-mcp-test-"));
|
|
332
|
+
const oldCwd = process.cwd();
|
|
333
|
+
process.chdir(tmpDir);
|
|
334
|
+
try {
|
|
335
|
+
const server = new McpServer("/test-sandbox", "Sandbox Test");
|
|
336
|
+
registerDevTools(server);
|
|
337
|
+
|
|
338
|
+
const resp = JSON.parse(server.handleMessage({
|
|
339
|
+
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
340
|
+
params: { name: "file_read", arguments: { path: "../../../etc/passwd" } },
|
|
341
|
+
}));
|
|
342
|
+
const hasError = resp.error !== undefined;
|
|
343
|
+
const errorMsg = resp.error?.message?.toLowerCase() ?? "";
|
|
344
|
+
assert(
|
|
345
|
+
"file_read sandbox — rejects path escape",
|
|
346
|
+
hasError && (errorMsg.includes("escapes") || errorMsg.includes("path")),
|
|
347
|
+
`Got: ${JSON.stringify(resp)}`,
|
|
348
|
+
);
|
|
349
|
+
} finally {
|
|
350
|
+
process.chdir(oldCwd);
|
|
351
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
{
|
|
356
|
+
McpServer._instances = [];
|
|
357
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tina4-mcp-test-"));
|
|
358
|
+
const oldCwd = process.cwd();
|
|
359
|
+
process.chdir(tmpDir);
|
|
360
|
+
try {
|
|
361
|
+
const server = new McpServer("/test-sandbox2", "Sandbox Test 2");
|
|
362
|
+
registerDevTools(server);
|
|
363
|
+
|
|
364
|
+
const resp = JSON.parse(server.handleMessage({
|
|
365
|
+
jsonrpc: "2.0", id: 1, method: "tools/call",
|
|
366
|
+
params: { name: "file_write", arguments: { path: "../../evil.txt", content: "hacked" } },
|
|
367
|
+
}));
|
|
368
|
+
assert(
|
|
369
|
+
"file_write sandbox — rejects path escape",
|
|
370
|
+
resp.error !== undefined,
|
|
371
|
+
`Got: ${JSON.stringify(resp)}`,
|
|
372
|
+
);
|
|
373
|
+
} finally {
|
|
374
|
+
process.chdir(oldCwd);
|
|
375
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Instance Registry ────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
console.log("\nInstance Registry");
|
|
382
|
+
|
|
383
|
+
{
|
|
384
|
+
McpServer._instances = [];
|
|
385
|
+
assert("instances — starts empty", McpServer._instances.length === 0);
|
|
386
|
+
new McpServer("/a", "Server A");
|
|
387
|
+
new McpServer("/b", "Server B");
|
|
388
|
+
assert("instances — two registered", McpServer._instances.length === 2);
|
|
389
|
+
McpServer._instances = [];
|
|
390
|
+
assert("instances — cleared", McpServer._instances.length === 0);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Summary ──────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
console.log(`\nMCP Tests: ${pass} passed, ${fail} failed`);
|
|
396
|
+
if (fail > 0) process.exit(1);
|