tina4-nodejs 3.10.30 → 3.10.32
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.32",
|
|
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"],
|
|
@@ -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);
|