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:
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user