Files
Mathieu 5195018361 feat(mcp): whitelist REST endpoints by x-mcp-exposure extension (#9875)
Replace the hard-coded CONFIRM_PATTERNS list in the MCP server with a per-endpoint exposure decision carried by an x-mcp-exposure OpenAPI vendor extension. Each REST endpoint now declares its MCP exposure (allow / confirm / deny) with @Extension('x-mcp-exposure', ...) next to its HTTP method decorator. A new local ESLint rule mcp/require-mcp-expose makes the decision mandatory: any new @Get/@Post/@Put/@Patch/@Delete method without an exposure tag fails lint.

The MCP parseSwagger now reads the extension and applies a default-deny policy:

    'allow' → exposed as a query/action tool
    'confirm' → exposed only when XO_MCP_ENABLE_ACTIONS=1, with confirm-token round-trip
    'deny' / absent → never exposed

Defense in depth is preserved: route-filter regex (binaries, streams, stats) is applied before the exposure check, so a binary endpoint mistakenly tagged 'allow' still stays out of the MCP surface. The confirm_token UUID + TTL machinery in risk-config.mts is untouched.

All 248 endpoints across 34 controllers have been annotated with conservative defaults (GET → allow, destructive POST/DELETE/PUT/PATCH → confirm, binary exports → deny).
2026-05-26 17:50:09 +02:00

96 lines
2.3 KiB
JSON

{
"entryFile": "./src/index.mts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["./src/**/*.controller.mts"],
"spec": {
"outputDirectory": "./open-api/spec",
"specVersion": 3,
"basePath": "/rest/v0",
"securityDefinitions": {
"token": {
"type": "apiKey",
"name": "authenticationToken",
"in": "cookie"
},
"basic": {
"type": "http",
"scheme": "basic"
}
},
"specMerging": "recursive",
"spec": {
"paths": {
"/pools/{id}/vms": {
"post": {
"requestBody": {
"required": true,
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
},
"/srs/{id}/vdis": {
"post": {
"requestBody": {
"required": true,
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
},
"/vdis/{id}.{format}": {
"put": {
"requestBody": {
"required": true,
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
},
"/docs/swagger.json": {
"get": {
"operationId": "swaggerSpec",
"description": "Returns the swagger json ready to use for REST API clients",
"responses": {
"200": {
"description": "Ok"
}
},
"tags": ["docs"],
"x-mcp-exposure": "deny"
}
}
}
}
},
"routes": {
"routesDir": "./src/open-api/routes",
"esm": true,
"middleware": "express",
"basePath": "/rest/v0",
"authenticationModule": "./src/middlewares/authentication.middleware.mts",
"iocModule": "./src/ioc/ioc.mts"
},
"compilerOptions": {
"strictNullChecks": true
}
}