-
Notifications
You must be signed in to change notification settings - Fork 9
Ochafik/add zip tool #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0d01c5f
b7c1428
cf64b56
13814ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ import { | |
} from "@modelcontextprotocol/sdk/types.js"; | ||
import { z } from "zod"; | ||
import { zodToJsonSchema } from "zod-to-json-schema"; | ||
import JSZip from "jszip"; | ||
|
||
type ToolInput = { | ||
type: "object"; | ||
|
@@ -79,6 +80,16 @@ const GetResourceReferenceSchema = z.object({ | |
.describe("ID of the resource to reference (1-100)"), | ||
}); | ||
|
||
const ZipResourcesInputSchema = z.object({ | ||
files: z | ||
.record(z.string().url().describe("URL of the file to include in the zip")) | ||
.describe("Mapping of file names to URLs to include in the zip"), | ||
outputType: z.enum([ | ||
'resourceLink', | ||
'resource' | ||
]).default('resource').describe("How the resulting zip file should be returned. 'resourceLink' returns a linked to a resource that can be read later, 'resource' returns a full resource object."), | ||
}); | ||
|
||
enum ToolName { | ||
ECHO = "echo", | ||
ADD = "add", | ||
|
@@ -87,6 +98,7 @@ enum ToolName { | |
GET_TINY_IMAGE = "getTinyImage", | ||
ANNOTATED_MESSAGE = "annotatedMessage", | ||
GET_RESOURCE_REFERENCE = "getResourceReference", | ||
ZIP_RESOURCES = "zip", | ||
} | ||
|
||
enum PromptName { | ||
|
@@ -118,6 +130,7 @@ export const createMcpServer = (): McpServerWrapper => { | |
); | ||
|
||
const subscriptions: Set<string> = new Set(); | ||
const transientResources: Map<string, Resource> = new Map(); | ||
|
||
// Set up update interval for subscribed resources | ||
const subsUpdateInterval = setInterval(() => { | ||
|
@@ -261,6 +274,12 @@ export const createMcpServer = (): McpServerWrapper => { | |
server.setRequestHandler(ReadResourceRequestSchema, async (request) => { | ||
const uri = request.params.uri; | ||
|
||
if (transientResources.has(uri)) { | ||
return { | ||
contents: [transientResources.get(uri)!], | ||
}; | ||
} | ||
|
||
if (uri.startsWith("test://static/resource/")) { | ||
const index = parseInt(uri.split("/").pop() ?? "", 10) - 1; | ||
if (index >= 0 && index < ALL_RESOURCES.length) { | ||
|
@@ -446,6 +465,12 @@ export const createMcpServer = (): McpServerWrapper => { | |
"Returns a resource reference that can be used by MCP clients", | ||
inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput, | ||
}, | ||
{ | ||
name: ToolName.ZIP_RESOURCES, | ||
description: | ||
"Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file. Supports multiple output formats: inlined data URI (default), resource link, or full resource object", | ||
inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput, | ||
}, | ||
]; | ||
|
||
return { tools }; | ||
|
@@ -624,6 +649,53 @@ export const createMcpServer = (): McpServerWrapper => { | |
return { content }; | ||
} | ||
|
||
if (name === ToolName.ZIP_RESOURCES) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for later: at some point we should migrate this whole server to the newer nicer API There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i.e. |
||
const { files, outputType } = ZipResourcesInputSchema.parse(args); | ||
const zip = new JSZip(); | ||
|
||
for (const [fileName, fileUrl] of Object.entries(files)) { | ||
try { | ||
const response = await fetch(fileUrl); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we might want to screen this to avoid SSRF attacks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we want to be careful handling the result / set some limit to avoid DoS attacks. E.g. getting us to download a 2GB file. |
||
if (!response.ok) { | ||
throw new Error( | ||
`Failed to fetch ${fileUrl}: ${response.statusText}` | ||
); | ||
} | ||
const arrayBuffer = await response.arrayBuffer(); | ||
zip.file(fileName, arrayBuffer); | ||
} catch (error) { | ||
throw new Error( | ||
`Error fetching file ${fileUrl}: ${error instanceof Error ? error.message : String(error)}` | ||
); | ||
} | ||
} | ||
|
||
const blob = await zip.generateAsync({ type: "base64" }); | ||
const mimeType = "application/zip"; | ||
const name = `out_${Date.now()}.zip`; | ||
const uri = `resource://${name}`; | ||
const resource: Resource = { uri, name, mimeType, blob }; | ||
if (outputType === "resource") { | ||
return { | ||
content: [{ | ||
type: "resource", | ||
resource | ||
}] | ||
}; | ||
} else if (outputType === 'resourceLink') { | ||
transientResources.set(uri, resource); | ||
return { | ||
content: [{ | ||
type: "resource_link", | ||
mimeType, | ||
uri | ||
}] | ||
}; | ||
} else { | ||
throw new Error(`Unknown outputType: ${outputType}`); | ||
} | ||
} | ||
|
||
throw new Error(`Unknown tool: ${name}`); | ||
}); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can do this with
node:zlib
(https://nodejs.org/api/zlib.html) so you don't need to add another dependency