velocious 1.0.693 → 1.0.694
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/configuration-types.js +7 -0
- package/build/configuration.js +61 -1
- package/build/routes/built-in/mcp/controller.js +63 -0
- package/build/src/configuration-types.d.ts +20 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +7 -1
- package/build/src/configuration.d.ts +24 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +57 -2
- package/build/src/routes/built-in/mcp/controller.d.ts +14 -0
- package/build/src/routes/built-in/mcp/controller.d.ts.map +1 -0
- package/build/src/routes/built-in/mcp/controller.js +55 -0
- package/package.json +1 -1
- package/src/configuration-types.js +7 -0
- package/src/configuration.js +61 -1
- package/src/routes/built-in/mcp/controller.js +63 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Controller from "../../../controller.js";
|
|
2
|
+
export default class BuiltInMcpController extends Controller {
|
|
3
|
+
/**
|
|
4
|
+
* Runs one JSON-RPC message of the MCP protocol over the HTTP transport.
|
|
5
|
+
* The request body is parsed into params by the HTTP request parser; the
|
|
6
|
+
* JSON-RPC envelope is extracted from it and answered exactly like the stdio
|
|
7
|
+
* transport. The request's `Authorization` header is forwarded as the
|
|
8
|
+
* request-context `authorization` key, so tool calls authenticate through the
|
|
9
|
+
* app's own tenant/ability resolvers just like regular frontend-model calls.
|
|
10
|
+
* @returns {Promise<void>} - Resolves when the JSON-RPC response has been rendered.
|
|
11
|
+
*/
|
|
12
|
+
handle(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../../src/routes/built-in/mcp/controller.js"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,wBAAwB,CAAA;AAG/C,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,UAAU;IAC1D;;;;;;;;OAQG;IACG,MAAM,IAFC,OAAO,CAAC,IAAI,CAAC,CA4BzB;CACF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Controller from "../../../controller.js";
|
|
2
|
+
import { handleMcpRequest } from "../../../mcp/json-rpc-server.js";
|
|
3
|
+
export default class BuiltInMcpController extends Controller {
|
|
4
|
+
/**
|
|
5
|
+
* Runs one JSON-RPC message of the MCP protocol over the HTTP transport.
|
|
6
|
+
* The request body is parsed into params by the HTTP request parser; the
|
|
7
|
+
* JSON-RPC envelope is extracted from it and answered exactly like the stdio
|
|
8
|
+
* transport. The request's `Authorization` header is forwarded as the
|
|
9
|
+
* request-context `authorization` key, so tool calls authenticate through the
|
|
10
|
+
* app's own tenant/ability resolvers just like regular frontend-model calls.
|
|
11
|
+
* @returns {Promise<void>} - Resolves when the JSON-RPC response has been rendered.
|
|
12
|
+
*/
|
|
13
|
+
async handle() {
|
|
14
|
+
const params = this.params();
|
|
15
|
+
const requestContext = buildRequestContextFromHeaders({ request: this.request() });
|
|
16
|
+
const response = await handleMcpRequest({
|
|
17
|
+
configuration: this.getConfiguration(),
|
|
18
|
+
request: {
|
|
19
|
+
id: params.id ?? null,
|
|
20
|
+
jsonrpc: params.jsonrpc,
|
|
21
|
+
method: params.method,
|
|
22
|
+
params: typeof params.params === "object" && params.params !== null && !Array.isArray(params.params) ? params.params : undefined
|
|
23
|
+
},
|
|
24
|
+
requestContext
|
|
25
|
+
});
|
|
26
|
+
this._response.setHeader("Content-Type", "application/json; charset=UTF-8");
|
|
27
|
+
if (response === null) {
|
|
28
|
+
// Notifications never carry a JSON-RPC response id; 202 Accepted with an
|
|
29
|
+
// empty body keeps the HTTP transport honest about that.
|
|
30
|
+
this._response.setStatus(202);
|
|
31
|
+
this._response.setBody("");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this._response.setBody(`${JSON.stringify(response)}\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Builds the remote request context forwarded to every tool call from the
|
|
39
|
+
* incoming HTTP request headers. The `Authorization` header becomes the
|
|
40
|
+
* request-context `authorization` key, which the MCP dispatcher forwards as
|
|
41
|
+
* the Authorization header of the synthetic in-process frontend-model request.
|
|
42
|
+
* @param {object} args - Arguments.
|
|
43
|
+
* @param {import("../../../http-server/client/request.js").default} args.request - Incoming HTTP request.
|
|
44
|
+
* @returns {Record<string, string | number | boolean> | undefined} - Request context, or undefined when no headers carry context.
|
|
45
|
+
*/
|
|
46
|
+
function buildRequestContextFromHeaders({ request }) {
|
|
47
|
+
/** @type {Record<string, string | number | boolean>} */
|
|
48
|
+
const context = {};
|
|
49
|
+
const authorization = request.header("authorization");
|
|
50
|
+
if (typeof authorization === "string" && authorization.trim().length > 0) {
|
|
51
|
+
context.authorization = authorization.trim();
|
|
52
|
+
}
|
|
53
|
+
return Object.keys(context).length > 0 ? context : undefined;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udHJvbGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9yb3V0ZXMvYnVpbHQtaW4vbWNwL2NvbnRyb2xsZXIuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxVQUFVLE1BQU0sd0JBQXdCLENBQUE7QUFDL0MsT0FBTyxFQUFDLGdCQUFnQixFQUFDLE1BQU0saUNBQWlDLENBQUE7QUFFaEUsTUFBTSxDQUFDLE9BQU8sT0FBTyxvQkFBcUIsU0FBUSxVQUFVO0lBQzFEOzs7Ozs7OztPQVFHO0lBQ0gsS0FBSyxDQUFDLE1BQU07UUFDVixNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUE7UUFDNUIsTUFBTSxjQUFjLEdBQUcsOEJBQThCLENBQUMsRUFBQyxPQUFPLEVBQUUsSUFBSSxDQUFDLE9BQU8sRUFBRSxFQUFDLENBQUMsQ0FBQTtRQUNoRixNQUFNLFFBQVEsR0FBRyxNQUFNLGdCQUFnQixDQUFDO1lBQ3RDLGFBQWEsRUFBRSxJQUFJLENBQUMsZ0JBQWdCLEVBQUU7WUFDdEMsT0FBTyxFQUFFO2dCQUNQLEVBQUUsRUFBRSxNQUFNLENBQUMsRUFBRSxJQUFJLElBQUk7Z0JBQ3JCLE9BQU8sRUFBRSxNQUFNLENBQUMsT0FBTztnQkFDdkIsTUFBTSxFQUFFLE1BQU0sQ0FBQyxNQUFNO2dCQUNyQixNQUFNLEVBQUUsT0FBTyxNQUFNLENBQUMsTUFBTSxLQUFLLFFBQVEsSUFBSSxNQUFNLENBQUMsTUFBTSxLQUFLLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxTQUFTO2FBQ2pJO1lBQ0QsY0FBYztTQUNmLENBQUMsQ0FBQTtRQUVGLElBQUksQ0FBQyxTQUFTLENBQUMsU0FBUyxDQUFDLGNBQWMsRUFBRSxpQ0FBaUMsQ0FBQyxDQUFBO1FBRTNFLElBQUksUUFBUSxLQUFLLElBQUksRUFBRSxDQUFDO1lBQ3RCLHlFQUF5RTtZQUN6RSx5REFBeUQ7WUFDekQsSUFBSSxDQUFDLFNBQVMsQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUE7WUFDN0IsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLENBQUE7WUFFMUIsT0FBTTtRQUNSLENBQUM7UUFFRCxJQUFJLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFBO0lBQ3pELENBQUM7Q0FDRjtBQUVEOzs7Ozs7OztHQVFHO0FBQ0gsU0FBUyw4QkFBOEIsQ0FBQyxFQUFDLE9BQU8sRUFBQztJQUMvQyx3REFBd0Q7SUFDeEQsTUFBTSxPQUFPLEdBQUcsRUFBRSxDQUFBO0lBRWxCLE1BQU0sYUFBYSxHQUFHLE9BQU8sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUFDLENBQUE7SUFFckQsSUFBSSxPQUFPLGFBQWEsS0FBSyxRQUFRLElBQUksYUFBYSxDQUFDLElBQUksRUFBRSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUN6RSxPQUFPLENBQUMsYUFBYSxHQUFHLGFBQWEsQ0FBQyxJQUFJLEVBQUUsQ0FBQTtJQUM5QyxDQUFDO0lBRUQsT0FBTyxNQUFNLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFBO0FBQzlELENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgQ29udHJvbGxlciBmcm9tIFwiLi4vLi4vLi4vY29udHJvbGxlci5qc1wiXG5pbXBvcnQge2hhbmRsZU1jcFJlcXVlc3R9IGZyb20gXCIuLi8uLi8uLi9tY3AvanNvbi1ycGMtc2VydmVyLmpzXCJcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgQnVpbHRJbk1jcENvbnRyb2xsZXIgZXh0ZW5kcyBDb250cm9sbGVyIHtcbiAgLyoqXG4gICAqIFJ1bnMgb25lIEpTT04tUlBDIG1lc3NhZ2Ugb2YgdGhlIE1DUCBwcm90b2NvbCBvdmVyIHRoZSBIVFRQIHRyYW5zcG9ydC5cbiAgICogVGhlIHJlcXVlc3QgYm9keSBpcyBwYXJzZWQgaW50byBwYXJhbXMgYnkgdGhlIEhUVFAgcmVxdWVzdCBwYXJzZXI7IHRoZVxuICAgKiBKU09OLVJQQyBlbnZlbG9wZSBpcyBleHRyYWN0ZWQgZnJvbSBpdCBhbmQgYW5zd2VyZWQgZXhhY3RseSBsaWtlIHRoZSBzdGRpb1xuICAgKiB0cmFuc3BvcnQuIFRoZSByZXF1ZXN0J3MgYEF1dGhvcml6YXRpb25gIGhlYWRlciBpcyBmb3J3YXJkZWQgYXMgdGhlXG4gICAqIHJlcXVlc3QtY29udGV4dCBgYXV0aG9yaXphdGlvbmAga2V5LCBzbyB0b29sIGNhbGxzIGF1dGhlbnRpY2F0ZSB0aHJvdWdoIHRoZVxuICAgKiBhcHAncyBvd24gdGVuYW50L2FiaWxpdHkgcmVzb2x2ZXJzIGp1c3QgbGlrZSByZWd1bGFyIGZyb250ZW5kLW1vZGVsIGNhbGxzLlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZTx2b2lkPn0gLSBSZXNvbHZlcyB3aGVuIHRoZSBKU09OLVJQQyByZXNwb25zZSBoYXMgYmVlbiByZW5kZXJlZC5cbiAgICovXG4gIGFzeW5jIGhhbmRsZSgpIHtcbiAgICBjb25zdCBwYXJhbXMgPSB0aGlzLnBhcmFtcygpXG4gICAgY29uc3QgcmVxdWVzdENvbnRleHQgPSBidWlsZFJlcXVlc3RDb250ZXh0RnJvbUhlYWRlcnMoe3JlcXVlc3Q6IHRoaXMucmVxdWVzdCgpfSlcbiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGhhbmRsZU1jcFJlcXVlc3Qoe1xuICAgICAgY29uZmlndXJhdGlvbjogdGhpcy5nZXRDb25maWd1cmF0aW9uKCksXG4gICAgICByZXF1ZXN0OiB7XG4gICAgICAgIGlkOiBwYXJhbXMuaWQgPz8gbnVsbCxcbiAgICAgICAganNvbnJwYzogcGFyYW1zLmpzb25ycGMsXG4gICAgICAgIG1ldGhvZDogcGFyYW1zLm1ldGhvZCxcbiAgICAgICAgcGFyYW1zOiB0eXBlb2YgcGFyYW1zLnBhcmFtcyA9PT0gXCJvYmplY3RcIiAmJiBwYXJhbXMucGFyYW1zICE9PSBudWxsICYmICFBcnJheS5pc0FycmF5KHBhcmFtcy5wYXJhbXMpID8gcGFyYW1zLnBhcmFtcyA6IHVuZGVmaW5lZFxuICAgICAgfSxcbiAgICAgIHJlcXVlc3RDb250ZXh0XG4gICAgfSlcblxuICAgIHRoaXMuX3Jlc3BvbnNlLnNldEhlYWRlcihcIkNvbnRlbnQtVHlwZVwiLCBcImFwcGxpY2F0aW9uL2pzb247IGNoYXJzZXQ9VVRGLThcIilcblxuICAgIGlmIChyZXNwb25zZSA9PT0gbnVsbCkge1xuICAgICAgLy8gTm90aWZpY2F0aW9ucyBuZXZlciBjYXJyeSBhIEpTT04tUlBDIHJlc3BvbnNlIGlkOyAyMDIgQWNjZXB0ZWQgd2l0aCBhblxuICAgICAgLy8gZW1wdHkgYm9keSBrZWVwcyB0aGUgSFRUUCB0cmFuc3BvcnQgaG9uZXN0IGFib3V0IHRoYXQuXG4gICAgICB0aGlzLl9yZXNwb25zZS5zZXRTdGF0dXMoMjAyKVxuICAgICAgdGhpcy5fcmVzcG9uc2Uuc2V0Qm9keShcIlwiKVxuXG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICB0aGlzLl9yZXNwb25zZS5zZXRCb2R5KGAke0pTT04uc3RyaW5naWZ5KHJlc3BvbnNlKX1cXG5gKVxuICB9XG59XG5cbi8qKlxuICogQnVpbGRzIHRoZSByZW1vdGUgcmVxdWVzdCBjb250ZXh0IGZvcndhcmRlZCB0byBldmVyeSB0b29sIGNhbGwgZnJvbSB0aGVcbiAqIGluY29taW5nIEhUVFAgcmVxdWVzdCBoZWFkZXJzLiBUaGUgYEF1dGhvcml6YXRpb25gIGhlYWRlciBiZWNvbWVzIHRoZVxuICogcmVxdWVzdC1jb250ZXh0IGBhdXRob3JpemF0aW9uYCBrZXksIHdoaWNoIHRoZSBNQ1AgZGlzcGF0Y2hlciBmb3J3YXJkcyBhc1xuICogdGhlIEF1dGhvcml6YXRpb24gaGVhZGVyIG9mIHRoZSBzeW50aGV0aWMgaW4tcHJvY2VzcyBmcm9udGVuZC1tb2RlbCByZXF1ZXN0LlxuICogQHBhcmFtIHtvYmplY3R9IGFyZ3MgLSBBcmd1bWVudHMuXG4gKiBAcGFyYW0ge2ltcG9ydChcIi4uLy4uLy4uL2h0dHAtc2VydmVyL2NsaWVudC9yZXF1ZXN0LmpzXCIpLmRlZmF1bHR9IGFyZ3MucmVxdWVzdCAtIEluY29taW5nIEhUVFAgcmVxdWVzdC5cbiAqIEByZXR1cm5zIHtSZWNvcmQ8c3RyaW5nLCBzdHJpbmcgfCBudW1iZXIgfCBib29sZWFuPiB8IHVuZGVmaW5lZH0gLSBSZXF1ZXN0IGNvbnRleHQsIG9yIHVuZGVmaW5lZCB3aGVuIG5vIGhlYWRlcnMgY2FycnkgY29udGV4dC5cbiAqL1xuZnVuY3Rpb24gYnVpbGRSZXF1ZXN0Q29udGV4dEZyb21IZWFkZXJzKHtyZXF1ZXN0fSkge1xuICAvKiogQHR5cGUge1JlY29yZDxzdHJpbmcsIHN0cmluZyB8IG51bWJlciB8IGJvb2xlYW4+fSAqL1xuICBjb25zdCBjb250ZXh0ID0ge31cblxuICBjb25zdCBhdXRob3JpemF0aW9uID0gcmVxdWVzdC5oZWFkZXIoXCJhdXRob3JpemF0aW9uXCIpXG5cbiAgaWYgKHR5cGVvZiBhdXRob3JpemF0aW9uID09PSBcInN0cmluZ1wiICYmIGF1dGhvcml6YXRpb24udHJpbSgpLmxlbmd0aCA+IDApIHtcbiAgICBjb250ZXh0LmF1dGhvcml6YXRpb24gPSBhdXRob3JpemF0aW9uLnRyaW0oKVxuICB9XG5cbiAgcmV0dXJuIE9iamVjdC5rZXlzKGNvbnRleHQpLmxlbmd0aCA+IDAgPyBjb250ZXh0IDogdW5kZWZpbmVkXG59XG4iXX0=
|
package/package.json
CHANGED
|
@@ -744,6 +744,12 @@
|
|
|
744
744
|
* @property {string} [path] - HTTP path for the built-in API manifest endpoint. Defaults to `/api/manifest`.
|
|
745
745
|
* @property {string} [token] - Bearer token required in the `Authorization: Bearer <token>` header. When set, requests without a matching token are not routed (404), so the endpoint stays hidden.
|
|
746
746
|
*/
|
|
747
|
+
/**
|
|
748
|
+
* @typedef {object} McpConfiguration
|
|
749
|
+
* @property {string} [path] - HTTP path for the built-in MCP endpoint. Defaults to `/mcp`.
|
|
750
|
+
* @property {string} [token] - Bearer token required in the `Authorization` header. When set, requests without a matching token are not routed (404), so the endpoint stays hidden.
|
|
751
|
+
*/
|
|
752
|
+
|
|
747
753
|
|
|
748
754
|
/**
|
|
749
755
|
* @typedef {object} DebugEndpointConfiguration
|
|
@@ -776,6 +782,7 @@
|
|
|
776
782
|
* @property {boolean} [debug] - Enable debug logging.
|
|
777
783
|
* @property {boolean | DebugEndpointConfiguration} [debugEndpoint] - Enable the built-in debug endpoint. Defaults to false.
|
|
778
784
|
* @property {boolean | ApiManifestConfiguration} [apiManifest] - Enable the built-in API manifest endpoint. Defaults to false.
|
|
785
|
+
* @property {boolean | McpConfiguration} [mcp] - Enable the built-in MCP endpoint on the HTTP server (JSON-RPC over HTTP in addition to the `velocious mcp` stdio server). Defaults to false.
|
|
779
786
|
* @property {string} [directory] - Base directory for the project.
|
|
780
787
|
* @property {boolean} [enforceTenantDatabaseScopes] - Require tenant-switched model queries to resolve a tenant database identifier. Defaults to true.
|
|
781
788
|
* @property {string} [environment] - Current environment name.
|
package/src/configuration.js
CHANGED
|
@@ -260,7 +260,7 @@ export default class VelociousConfiguration {
|
|
|
260
260
|
* Runs constructor.
|
|
261
261
|
* @param {import("./configuration-types.js").ConfigurationArgsType} args - Configuration arguments.
|
|
262
262
|
*/
|
|
263
|
-
constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients, frontendTenantSqlite, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
|
|
263
|
+
constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, apiManifest = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients, frontendTenantSqlite, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, mcp = false, packages, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, secureFrontendModelErrors, structureSql, sync, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
|
|
264
264
|
restArgsError(restArgs)
|
|
265
265
|
|
|
266
266
|
this._abilityResolver = abilityResolver
|
|
@@ -304,6 +304,7 @@ export default class VelociousConfiguration {
|
|
|
304
304
|
this.debug = debug
|
|
305
305
|
this._debugEndpoint = this._normalizeDebugEndpoint(debugEndpoint)
|
|
306
306
|
this._apiManifest = this._normalizeApiManifest(apiManifest)
|
|
307
|
+
this._mcp = this._normalizeMcp(mcp)
|
|
307
308
|
this._environment = environment || globalThis.process?.env.VELOCIOUS_ENV || globalThis.process?.env.NODE_ENV || "development"
|
|
308
309
|
this._environmentHandler = environmentHandler
|
|
309
310
|
this._enforceTenantDatabaseScopes = enforceTenantDatabaseScopes
|
|
@@ -486,6 +487,7 @@ export default class VelociousConfiguration {
|
|
|
486
487
|
this._routeResolverHooks = [...(routeResolverHooks || [])]
|
|
487
488
|
this._addDebugEndpointRouteHook()
|
|
488
489
|
this._addApiManifestRouteHook()
|
|
490
|
+
this._addMcpRouteHook()
|
|
489
491
|
|
|
490
492
|
/**
|
|
491
493
|
* Stores the applied route mounts value.
|
|
@@ -625,6 +627,63 @@ export default class VelociousConfiguration {
|
|
|
625
627
|
})
|
|
626
628
|
}
|
|
627
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Runs normalize mcp.
|
|
632
|
+
* @param {boolean | {path?: string, token?: string}} value - MCP endpoint configuration.
|
|
633
|
+
* @returns {{enabled: boolean, path: string, token: string | null}} - Normalized MCP endpoint configuration.
|
|
634
|
+
*/
|
|
635
|
+
_normalizeMcp(value) {
|
|
636
|
+
if (value === false || value === undefined) return {enabled: false, path: "/mcp", token: null}
|
|
637
|
+
if (value === true) return {enabled: true, path: "/mcp", token: null}
|
|
638
|
+
|
|
639
|
+
if (typeof value !== "object" || value === null) {
|
|
640
|
+
throw new Error(`Expected mcp to be a boolean or object, got: ${String(value)}`)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const path = value.path || "/mcp"
|
|
644
|
+
|
|
645
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
646
|
+
throw new Error(`Expected mcp.path to be a string starting with '/', got: ${String(path)}`)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const token = value.token === undefined || value.token === null ? null : value.token
|
|
650
|
+
|
|
651
|
+
if (token !== null && (typeof token !== "string" || !token.trim())) {
|
|
652
|
+
throw new Error(`Expected mcp.token to be a non-empty string, got: ${String(token)}`)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return {enabled: true, path, token: token === null ? null : token.trim()}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Runs add mcp route hook.
|
|
660
|
+
* @returns {void} - No return value.
|
|
661
|
+
*/
|
|
662
|
+
_addMcpRouteHook() {
|
|
663
|
+
if (!this._mcp.enabled) return
|
|
664
|
+
|
|
665
|
+
this.addRouteResolverHook(({currentPath, request}) => {
|
|
666
|
+
if (request.httpMethod() !== "POST") return null
|
|
667
|
+
if (currentPath !== this._mcp.path) return null
|
|
668
|
+
|
|
669
|
+
// When a token is configured, an unauthenticated request gets no route at
|
|
670
|
+
// all (404) rather than a 401, so the endpoint's existence stays hidden.
|
|
671
|
+
if (this._mcp.token && !this.debugEndpointRequestAuthorized(request, this._mcp.token)) return null
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
action: "handle",
|
|
675
|
+
controller: "velociousMcp",
|
|
676
|
+
controllerPath: "./built-in/mcp/controller.js",
|
|
677
|
+
// Tenant and ability resolution belong to the frontend-model dispatcher
|
|
678
|
+
// per command (withFrontendModelRequestContext), exactly like the regular
|
|
679
|
+
// /frontend-models route — the MCP transport never bypasses them.
|
|
680
|
+
skipAbilityResolution: true,
|
|
681
|
+
skipTenantResolution: true,
|
|
682
|
+
viewPath: "./built-in/mcp"
|
|
683
|
+
}
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
|
|
628
687
|
/**
|
|
629
688
|
* Runs add debug endpoint route hook.
|
|
630
689
|
* @returns {void} - No return value.
|
|
@@ -1042,6 +1101,7 @@ export default class VelociousConfiguration {
|
|
|
1042
1101
|
enforceTenantDatabaseScopes: this.getEnforceTenantDatabaseScopes(),
|
|
1043
1102
|
exposeInternalErrorsToClients: this.getExposeInternalErrorsToClients(),
|
|
1044
1103
|
initialized: this._isInitialized,
|
|
1104
|
+
mcp: this._mcp.enabled ? {enabled: true, path: this._mcp.path, tokenConfigured: Boolean(this._mcp.token)} : {enabled: false},
|
|
1045
1105
|
logging: {
|
|
1046
1106
|
debugLowLevel: this._logging?.debugLowLevel === true,
|
|
1047
1107
|
outputs: this._logging ? Object.keys(this._logging) : []
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import Controller from "../../../controller.js"
|
|
2
|
+
import {handleMcpRequest} from "../../../mcp/json-rpc-server.js"
|
|
3
|
+
|
|
4
|
+
export default class BuiltInMcpController extends Controller {
|
|
5
|
+
/**
|
|
6
|
+
* Runs one JSON-RPC message of the MCP protocol over the HTTP transport.
|
|
7
|
+
* The request body is parsed into params by the HTTP request parser; the
|
|
8
|
+
* JSON-RPC envelope is extracted from it and answered exactly like the stdio
|
|
9
|
+
* transport. The request's `Authorization` header is forwarded as the
|
|
10
|
+
* request-context `authorization` key, so tool calls authenticate through the
|
|
11
|
+
* app's own tenant/ability resolvers just like regular frontend-model calls.
|
|
12
|
+
* @returns {Promise<void>} - Resolves when the JSON-RPC response has been rendered.
|
|
13
|
+
*/
|
|
14
|
+
async handle() {
|
|
15
|
+
const params = this.params()
|
|
16
|
+
const requestContext = buildRequestContextFromHeaders({request: this.request()})
|
|
17
|
+
const response = await handleMcpRequest({
|
|
18
|
+
configuration: this.getConfiguration(),
|
|
19
|
+
request: {
|
|
20
|
+
id: params.id ?? null,
|
|
21
|
+
jsonrpc: params.jsonrpc,
|
|
22
|
+
method: params.method,
|
|
23
|
+
params: typeof params.params === "object" && params.params !== null && !Array.isArray(params.params) ? params.params : undefined
|
|
24
|
+
},
|
|
25
|
+
requestContext
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
this._response.setHeader("Content-Type", "application/json; charset=UTF-8")
|
|
29
|
+
|
|
30
|
+
if (response === null) {
|
|
31
|
+
// Notifications never carry a JSON-RPC response id; 202 Accepted with an
|
|
32
|
+
// empty body keeps the HTTP transport honest about that.
|
|
33
|
+
this._response.setStatus(202)
|
|
34
|
+
this._response.setBody("")
|
|
35
|
+
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this._response.setBody(`${JSON.stringify(response)}\n`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds the remote request context forwarded to every tool call from the
|
|
45
|
+
* incoming HTTP request headers. The `Authorization` header becomes the
|
|
46
|
+
* request-context `authorization` key, which the MCP dispatcher forwards as
|
|
47
|
+
* the Authorization header of the synthetic in-process frontend-model request.
|
|
48
|
+
* @param {object} args - Arguments.
|
|
49
|
+
* @param {import("../../../http-server/client/request.js").default} args.request - Incoming HTTP request.
|
|
50
|
+
* @returns {Record<string, string | number | boolean> | undefined} - Request context, or undefined when no headers carry context.
|
|
51
|
+
*/
|
|
52
|
+
function buildRequestContextFromHeaders({request}) {
|
|
53
|
+
/** @type {Record<string, string | number | boolean>} */
|
|
54
|
+
const context = {}
|
|
55
|
+
|
|
56
|
+
const authorization = request.header("authorization")
|
|
57
|
+
|
|
58
|
+
if (typeof authorization === "string" && authorization.trim().length > 0) {
|
|
59
|
+
context.authorization = authorization.trim()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Object.keys(context).length > 0 ? context : undefined
|
|
63
|
+
}
|