feat(mcp): add snippet CRUD tools to MCP server

- register list_snippets, get_snippet, create_snippet, update_snippet, delete_snippet
- inject SnippetService into McpService and import SnippetModule into McpModule
- cover all five tools with integration tests including error cases
This commit is contained in:
2026-04-20 16:21:52 +03:00
parent 4e494ce4fd
commit ba46789226
5 changed files with 373 additions and 10 deletions
+169
View File
@@ -11,6 +11,7 @@ import { CredentialService } from "../credential/credential.service";
import { BrowserService } from "../browser/browser.service";
import { CodeExecutorService } from "../code-executor/code-executor.service";
import { ScenarioService } from "../scenario/scenario.service";
import { SnippetService } from "../snippet/snippet.service";
import pkg from "../../package.json";
@@ -24,6 +25,7 @@ export class McpService {
private readonly browserService: BrowserService,
private readonly codeExecutor: CodeExecutorService,
private readonly scenarioService: ScenarioService,
private readonly snippetService: SnippetService,
) {}
private createServer(): McpServer {
@@ -1060,6 +1062,173 @@ export class McpService {
},
);
// ── Snippets ──────────────────────────────────────────────────────────────
server.registerTool(
"list_snippets",
{
description: "List all snippets (paginated)",
inputSchema: {
page: z
.number()
.int()
.min(1)
.optional()
.describe("Page number (default 1)"),
limit: z
.number()
.int()
.min(1)
.optional()
.describe("Items per page (default 50)"),
orderBy: z
.enum(["id", "alias", "title", "createdAt", "updatedAt"])
.optional()
.describe("Field to order by (default id)"),
orderDir: z
.enum(["ASC", "DESC"])
.optional()
.describe("Sort direction (default ASC)"),
},
},
async ({ page, limit, orderBy, orderDir }) => {
const result = await this.snippetService.findAll({
page,
limit,
orderBy,
orderDir,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(result) }],
};
},
);
server.registerTool(
"get_snippet",
{
description: "Get a snippet by ID",
inputSchema: {
id: z.uuid().describe("Snippet ID"),
},
},
async ({ id }) => {
try {
const snippet = await this.snippetService.findOne(id);
return {
content: [{ type: "text" as const, text: JSON.stringify(snippet) }],
};
} catch (err) {
return {
isError: true,
content: [{ type: "text" as const, text: (err as Error).message }],
};
}
},
);
server.registerTool(
"create_snippet",
{
description: "Create a new reusable code snippet",
inputSchema: {
alias: z
.string()
.describe(
"Unique identifier used to invoke the snippet via context.runSnippet(alias, ...args)",
),
title: z.string().describe("Human-readable title"),
description: z
.string()
.optional()
.describe("Optional markdown description"),
code: z
.string()
.describe(
"Async JavaScript body. Receives the same context as exec steps plus any positional ...args.",
),
},
},
async ({ alias, title, description, code }) => {
try {
const snippet = await this.snippetService.create({
alias,
title,
description,
code,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(snippet) }],
};
} catch (err) {
return {
isError: true,
content: [{ type: "text" as const, text: (err as Error).message }],
};
}
},
);
server.registerTool(
"update_snippet",
{
description: "Update an existing snippet",
inputSchema: {
id: z.uuid().describe("Snippet ID to update"),
alias: z.string().optional().describe("New alias"),
title: z.string().optional().describe("New title"),
description: z
.string()
.optional()
.describe("New markdown description"),
code: z.string().optional().describe("New code body"),
},
},
async ({ id, alias, title, description, code }) => {
try {
const snippet = await this.snippetService.update(id, {
alias,
title,
description,
code,
});
return {
content: [{ type: "text" as const, text: JSON.stringify(snippet) }],
};
} catch (err) {
return {
isError: true,
content: [{ type: "text" as const, text: (err as Error).message }],
};
}
},
);
server.registerTool(
"delete_snippet",
{
description: "Delete a snippet by ID",
inputSchema: {
id: z.uuid().describe("Snippet ID to delete"),
},
},
async ({ id }) => {
try {
await this.snippetService.remove(id);
return {
content: [
{ type: "text" as const, text: `Snippet ${id} deleted` },
],
};
} catch (err) {
return {
isError: true,
content: [{ type: "text" as const, text: (err as Error).message }],
};
}
},
);
// ── Transport ─────────────────────────────────────────────────────────────
}