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.
@@ -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
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.693",
6
+ "version": "1.0.694",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -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.
@@ -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
+ }