Add the media MCP tools and live-verified slopbot path

This commit is contained in:
Fisto
2026-09-16 09:54:22 +02:00
parent c81ae0d0fe
commit d483a2fa47
9 changed files with 179 additions and 17 deletions
+101 -1
View File
@@ -317,7 +317,16 @@ const READ_ONLY_TOOLS: Record<string, (args: any) => Promise<unknown> | unknown>
get_action_log: args => requirePuppet().readActionLog(args?.limit ?? 100),
get_puppet_config: () => requirePuppet().getConfig(),
whitelist_list: () => ({ ownerId: requirePuppet().owner, whitelist: requirePuppet().whitelist }),
read_messages: args => paced(() => requireCore().readMessages(args?.channelId, args?.limit ?? 20))
read_messages: args => paced(() => requireCore().readMessages(args?.channelId, args?.limit ?? 20)),
list_media_scrapes: () => media.listScrapes(),
get_media_storage: () => media.refreshRetention(),
query_media: args =>
media.queryMedia({
kind: args?.kind,
scrapeId: args?.scrapeId,
limit: args?.limit,
offset: args?.offset
})
};
function buildServer(): McpServer {
@@ -749,6 +758,97 @@ function buildServer(): McpServer {
async ({ userIds }) => jsonText({ whitelist: requirePuppet().removeFromWhitelist(userIds) })
);
server.registerTool(
"scrape_media",
{
description:
"Start a media scrape of a channel. Uses slopbot for guild channels it is in, otherwise the client. Near the storage limit it fails with MEDIA_LIMIT unless confirm is true.",
inputSchema: {
channelId: z.string().describe("Channel ID to scrape"),
guildId: z.string().optional().describe("Guild ID; slopbot needs it to match"),
mode: z
.enum(["links", "download"])
.optional()
.describe("links indexes URLs (default); download fetches attachment bytes"),
confirm: z.boolean().optional().describe("Accept the near-limit warning")
}
},
async ({ channelId, guildId, mode, confirm }) =>
jsonText(await media.startScrape({ channelId, guildId: guildId ?? null, mode, confirm }))
);
server.registerTool(
"list_media_scrapes",
{ description: "List media scrapes with state, pages, messages, and media counts." },
async () => jsonText(media.listScrapes())
);
server.registerTool(
"get_media_scrape",
{
description: "Get one media scrape by ID.",
inputSchema: { scrapeId: z.string().describe("Scrape ID") }
},
async ({ scrapeId }) => jsonText(media.getScrape(scrapeId))
);
server.registerTool(
"stop_media_scrape",
{
description: "Stop a media scrape. It pauses after the current page.",
inputSchema: { scrapeId: z.string().describe("Scrape ID") }
},
async ({ scrapeId }) => {
media.stopScrape(scrapeId);
return jsonText({ stopped: scrapeId });
}
);
server.registerTool(
"get_media_storage",
{
description:
"Media storage usage against the limit, per-kind sizes, and the scrape list. Marks old scrapes when over the limit."
},
async () => jsonText(media.refreshRetention())
);
server.registerTool(
"query_media",
{
description: "Query media items across scrapes, newest scrape first.",
inputSchema: {
kind: z.enum(["image", "gif", "video", "audio"]).optional(),
scrapeId: z.string().optional().describe("Restrict to one scrape"),
limit: z.number().int().min(1).max(500).optional(),
offset: z.number().int().min(0).optional()
}
},
async ({ kind, scrapeId, limit, offset }) =>
jsonText(media.queryMedia({ kind, scrapeId, limit, offset }))
);
server.registerTool(
"get_media_item",
{
description: "Get one media item by scrape ID and item ID.",
inputSchema: {
scrapeId: z.string().describe("Scrape ID"),
itemId: z.number().int().min(1).describe("Media row ID inside the scrape")
}
},
async ({ scrapeId, itemId }) => jsonText(media.getMediaItem(scrapeId, itemId))
);
server.registerTool(
"delete_media_scrape",
{
description: "Delete a scrape and its files. Irreversible.",
inputSchema: { scrapeId: z.string().describe("Scrape ID") }
},
async ({ scrapeId }) => jsonText({ deleted: media.cleanMedia([scrapeId]) === 1 })
);
return server;
}