MCP-Server für Buchhaltung und Organisation ergänzen
Fügt einen geschützten MCP-JSON-RPC-Endpunkt mit Buchhaltungs-Tools und Aufgaben-Tools hinzu. Berechtigungen werden rollenbasiert pro Mandant geprüft und die Auth-Logik berücksichtigt nun alle Rollen eines Nutzers.
This commit is contained in:
144
backend/src/routes/mcp.ts
Normal file
144
backend/src/routes/mcp.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { assertToolPermission, createMcpContext } from "../mcp/authz"
|
||||
import { mcpToolMap, mcpTools } from "../mcp/registry"
|
||||
import { asToolError, asToolResult } from "../mcp/result"
|
||||
import { JsonRpcRequest } from "../mcp/types"
|
||||
|
||||
const SUPPORTED_PROTOCOL_VERSIONS = [
|
||||
"2025-11-25",
|
||||
"2025-06-18",
|
||||
"2025-03-26",
|
||||
"2024-11-05",
|
||||
]
|
||||
|
||||
function jsonRpcResult(id: JsonRpcRequest["id"], result: unknown) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
function jsonRpcError(id: JsonRpcRequest["id"], code: number, message: string) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: id ?? null,
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function selectProtocolVersion(clientVersion?: string) {
|
||||
if (clientVersion && SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)) {
|
||||
return clientVersion
|
||||
}
|
||||
|
||||
return SUPPORTED_PROTOCOL_VERSIONS[0]
|
||||
}
|
||||
|
||||
export default async function mcpRoutes(server: FastifyInstance) {
|
||||
server.post("/mcp", async (req, reply) => {
|
||||
const body = req.body as JsonRpcRequest | JsonRpcRequest[]
|
||||
const requests = Array.isArray(body) ? body : [body]
|
||||
const responses = []
|
||||
|
||||
for (const request of requests) {
|
||||
const id = request?.id
|
||||
|
||||
if (!request || request.jsonrpc !== "2.0" || !request.method) {
|
||||
responses.push(jsonRpcError(id, -32600, "Invalid JSON-RPC request"))
|
||||
continue
|
||||
}
|
||||
|
||||
if (request.method === "notifications/initialized") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (request.method === "initialize") {
|
||||
const clientVersion = request.params?.protocolVersion
|
||||
|
||||
responses.push(jsonRpcResult(id, {
|
||||
protocolVersion: selectProtocolVersion(clientVersion),
|
||||
capabilities: {
|
||||
tools: {
|
||||
listChanged: false,
|
||||
},
|
||||
},
|
||||
serverInfo: {
|
||||
name: "fedeo-mcp",
|
||||
version: "1.0.0",
|
||||
},
|
||||
instructions: "FEDEO MCP-Server für mandantenbezogene Buchhaltungs- und Organisationswerkzeuge. Alle Tools prüfen Rollenberechtigungen und arbeiten im aktiven Mandanten.",
|
||||
}))
|
||||
continue
|
||||
}
|
||||
|
||||
if (request.method === "ping") {
|
||||
responses.push(jsonRpcResult(id, {}))
|
||||
continue
|
||||
}
|
||||
|
||||
if (request.method === "tools/list") {
|
||||
responses.push(jsonRpcResult(id, {
|
||||
tools: mcpTools.map((tool) => ({
|
||||
name: tool.name,
|
||||
title: tool.title,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: !tool.requiredPermissions.some((permission) => permission.endsWith(".write")),
|
||||
},
|
||||
})),
|
||||
}))
|
||||
continue
|
||||
}
|
||||
|
||||
if (request.method === "tools/call") {
|
||||
const toolName = request.params?.name
|
||||
const tool = typeof toolName === "string" ? mcpToolMap.get(toolName) : null
|
||||
|
||||
if (!tool) {
|
||||
responses.push(jsonRpcError(id, -32602, `Unknown tool: ${toolName}`))
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const context = await createMcpContext(server, req)
|
||||
assertToolPermission(context, tool)
|
||||
|
||||
const result = await tool.handler(context, request.params?.arguments || {})
|
||||
responses.push(jsonRpcResult(id, asToolResult(result)))
|
||||
} catch (error) {
|
||||
const statusCode = (error as any)?.statusCode
|
||||
|
||||
if (statusCode === 401 || statusCode === 403) {
|
||||
responses.push(jsonRpcError(id, statusCode === 401 ? -32001 : -32003, error instanceof Error ? error.message : "Forbidden"))
|
||||
} else {
|
||||
responses.push(jsonRpcResult(id, asToolError(error)))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
responses.push(jsonRpcError(id, -32601, `Method not found: ${request.method}`))
|
||||
}
|
||||
|
||||
if (responses.length === 0) {
|
||||
return reply.code(204).send()
|
||||
}
|
||||
|
||||
return Array.isArray(body) ? responses : responses[0]
|
||||
})
|
||||
|
||||
server.get("/mcp", async (_req, reply) => {
|
||||
return reply.send({
|
||||
name: "fedeo-mcp",
|
||||
transport: "http-json-rpc",
|
||||
endpoint: "/api/mcp",
|
||||
tools: mcpTools.map((tool) => tool.name),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user