mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 01:45:36 +02:00
test(llm): test MCP using supertest
This commit is contained in:
143
apps/server/spec/etapi/mcp.spec.ts
Normal file
143
apps/server/spec/etapi/mcp.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { createNote, login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
import optionService from "../../src/services/options.js";
|
||||
import cls from "../../src/services/cls.js";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
const MCP_ACCEPT = "application/json, text/event-stream";
|
||||
|
||||
/** Builds a JSON-RPC 2.0 request body for MCP. */
|
||||
function jsonRpc(method: string, params?: Record<string, unknown>, id: number = 1) {
|
||||
return { jsonrpc: "2.0", id, method, params };
|
||||
}
|
||||
|
||||
/** Parses the JSON-RPC response from an SSE response text. */
|
||||
function parseSseResponse(text: string) {
|
||||
const dataLine = text.split("\n").find(line => line.startsWith("data: "));
|
||||
if (!dataLine) {
|
||||
throw new Error(`No SSE data line found in response: ${text}`);
|
||||
}
|
||||
return JSON.parse(dataLine.slice("data: ".length));
|
||||
}
|
||||
|
||||
function mcpPost(app: Application) {
|
||||
return supertest(app)
|
||||
.post("/mcp")
|
||||
.set("Accept", MCP_ACCEPT)
|
||||
.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
function setOption(name: Parameters<typeof optionService.setOption>[0], value: string) {
|
||||
cls.init(() => optionService.setOption(name, value));
|
||||
}
|
||||
|
||||
describe("mcp", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
});
|
||||
|
||||
describe("option gate", () => {
|
||||
it("rejects requests when mcpEnabled is false", async () => {
|
||||
setOption("mcpEnabled", "false");
|
||||
|
||||
const response = await mcpPost(app)
|
||||
.send(jsonRpc("initialize"))
|
||||
.expect(403);
|
||||
|
||||
expect(response.body.error).toContain("disabled");
|
||||
});
|
||||
|
||||
it("accepts requests when mcpEnabled is true", async () => {
|
||||
setOption("mcpEnabled", "true");
|
||||
|
||||
const response = await mcpPost(app)
|
||||
.send(jsonRpc("initialize", {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "test", version: "1.0.0" }
|
||||
}));
|
||||
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("protocol", () => {
|
||||
beforeAll(() => {
|
||||
setOption("mcpEnabled", "true");
|
||||
});
|
||||
|
||||
it("initializes and returns server capabilities", async () => {
|
||||
const response = await mcpPost(app)
|
||||
.send(jsonRpc("initialize", {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "test", version: "1.0.0" }
|
||||
}))
|
||||
.expect(200);
|
||||
|
||||
const body = parseSseResponse(response.text);
|
||||
expect(body.result.serverInfo.name).toBe("trilium-notes");
|
||||
expect(body.result.capabilities.tools).toBeDefined();
|
||||
});
|
||||
|
||||
it("lists available tools", async () => {
|
||||
const response = await mcpPost(app)
|
||||
.send(jsonRpc("tools/list"))
|
||||
.expect(200);
|
||||
|
||||
const body = parseSseResponse(response.text);
|
||||
const toolNames: string[] = body.result.tools.map((t: { name: string }) => t.name);
|
||||
expect(toolNames).toContain("search_notes");
|
||||
expect(toolNames).toContain("read_note");
|
||||
expect(toolNames).toContain("create_note");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tools", () => {
|
||||
let noteId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
setOption("mcpEnabled", "true");
|
||||
noteId = await createNote(app, token, "MCP test note content");
|
||||
});
|
||||
|
||||
it("searches for notes", async () => {
|
||||
const response = await mcpPost(app)
|
||||
.send(jsonRpc("tools/call", {
|
||||
name: "search_notes",
|
||||
arguments: { query: "MCP test note content" }
|
||||
}))
|
||||
.expect(200);
|
||||
|
||||
const body = parseSseResponse(response.text);
|
||||
expect(body.result).toBeDefined();
|
||||
const content = body.result.content;
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
expect(content[0].text).toContain(noteId);
|
||||
});
|
||||
|
||||
it("reads a note by ID", async () => {
|
||||
const response = await mcpPost(app)
|
||||
.send(jsonRpc("tools/call", {
|
||||
name: "read_note",
|
||||
arguments: { noteId }
|
||||
}))
|
||||
.expect(200);
|
||||
|
||||
const body = parseSseResponse(response.text);
|
||||
expect(body.result).toBeDefined();
|
||||
const parsed = JSON.parse(body.result.content[0].text);
|
||||
expect(parsed.noteId).toBe(noteId);
|
||||
expect(parsed.content).toContain("MCP test note content");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user