feat(mcp): Dateien ohne Base64 per HTTP hochladen
- POST /api/mcp/files nimmt multipart/form-data oder Rohdaten mit dem MCP-Token an, Ziel Dokumentenablage oder Eingangsrechnung - documents.files.upload_url liefert Adresse, Parameter und curl-Beispiele - Upload einer Eingangsrechnung bereitet nur noch die hochgeladene Datei vor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,11 @@ import { s3 } from "../utils/s3"
|
||||
import { secrets } from "../utils/secrets"
|
||||
import { GetObjectCommand } from "@aws-sdk/client-s3"
|
||||
import { sanitizeFilename } from "../utils/filename"
|
||||
import multipart from "@fastify/multipart"
|
||||
import { MCP_FILE_LIMIT } from "../mcp/documentFiles"
|
||||
import { parseUploadQuery, storeMcpFileUpload } from "../mcp/fileUploads"
|
||||
|
||||
const RAW_UPLOAD_TYPES = ["application/pdf", "application/octet-stream", "application/xml", "text/xml", "image/jpeg", "image/png", "image/tiff"]
|
||||
|
||||
const SUPPORTED_PROTOCOL_VERSIONS = [
|
||||
"2025-11-25",
|
||||
@@ -62,7 +67,13 @@ const requireMcpTokenManagementPermission = (req: any) => {
|
||||
throw Object.assign(new Error("Fehlende Berechtigung: mcp.tokens.write"), { statusCode: 403 })
|
||||
}
|
||||
|
||||
export default async function mcpRoutes(server: FastifyInstance) {
|
||||
type McpRouteOptions = {
|
||||
/** Replaceable in tests to avoid object storage. */
|
||||
storeUpload?: typeof storeMcpFileUpload
|
||||
}
|
||||
|
||||
export default async function mcpRoutes(server: FastifyInstance, options: McpRouteOptions = {}) {
|
||||
const storeUpload = options.storeUpload || storeMcpFileUpload
|
||||
server.get("/mcp/tokens", async (req, reply) => {
|
||||
try {
|
||||
requireMcpTokenManagementPermission(req)
|
||||
@@ -221,6 +232,37 @@ export default async function mcpRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
// Binary uploads without Base64; parsers stay scoped so JSON-RPC parsing is unaffected.
|
||||
await server.register(async (uploads) => {
|
||||
await uploads.register(multipart, { limits: { fileSize: MCP_FILE_LIMIT, files: 1 } })
|
||||
uploads.addContentTypeParser(RAW_UPLOAD_TYPES, { parseAs: "buffer", bodyLimit: MCP_FILE_LIMIT }, (_req, body, done) => done(null, body))
|
||||
|
||||
uploads.post("/mcp/files", async (req, reply) => {
|
||||
try {
|
||||
const context = await createMcpContext(server, req)
|
||||
const query = parseUploadQuery(req.query as Record<string, unknown>)
|
||||
let body: Buffer
|
||||
let filename = query.filename
|
||||
let mimeType = req.headers["content-type"]?.split(";")[0].trim() || null
|
||||
if (req.isMultipart()) {
|
||||
const part = await req.file()
|
||||
if (!part || part.fieldname !== "file") return reply.code(400).send({ error: "Bitte die Datei im Feld file übertragen." })
|
||||
body = await part.toBuffer()
|
||||
filename ||= part.filename
|
||||
mimeType = part.mimetype
|
||||
} else if (Buffer.isBuffer(req.body)) {
|
||||
body = req.body
|
||||
} else {
|
||||
return reply.code(415).send({ error: `Bitte multipart/form-data oder einen der Content-Types ${RAW_UPLOAD_TYPES.join(", ")} verwenden.` })
|
||||
}
|
||||
if (!filename) return reply.code(400).send({ error: "filename ist erforderlich." })
|
||||
return await storeUpload(context, { ...query, filename, body, mimeType })
|
||||
} catch (error) {
|
||||
return reply.code((error as any)?.statusCode || 400).send({ error: error instanceof Error ? error.message : "Upload fehlgeschlagen." })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
server.post("/mcp", { bodyLimit: MCP_BODY_LIMIT }, async (req, reply) => {
|
||||
const body = req.body as JsonRpcRequest | JsonRpcRequest[]
|
||||
const requests = Array.isArray(body) ? body : [body]
|
||||
|
||||
Reference in New Issue
Block a user