@dereekb/firebase-server 13.36.0 → 13.37.0
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/calcom/package.json +10 -10
- package/discord/package.json +10 -10
- package/index.cjs.js +1 -1
- package/index.esm.js +1 -1
- package/mailgun/package.json +9 -9
- package/mcp/index.cjs.js +282 -63
- package/mcp/index.esm.js +280 -61
- package/mcp/package.json +13 -12
- package/mcp/src/lib/controller/mcp.controller.d.ts +28 -3
- package/mcp/src/lib/controller/mcp.wellknown.controller.d.ts +21 -1
- package/mcp/src/lib/mcp.config.d.ts +9 -7
- package/mcp/src/lib/mcp.module.d.ts +2 -1
- package/mcp/src/lib/service/mcp.server.factory.d.ts +5 -4
- package/mcp/src/lib/service/mcp.tool-generator.d.ts +1 -1
- package/mcp/src/lib/service/mcp.visibility.d.ts +1 -1
- package/mcp/src/lib/transport/streamable-http.transport.d.ts +16 -11
- package/model/index.cjs.js +1 -1
- package/model/index.esm.js +1 -1
- package/model/package.json +9 -9
- package/oidc/index.cjs.js +257 -67
- package/oidc/index.esm.js +256 -69
- package/oidc/package.json +10 -10
- package/oidc/src/lib/profile.d.ts +21 -0
- package/oidc/src/lib/service/index.d.ts +1 -0
- package/oidc/src/lib/service/oidc.config.service.d.ts +14 -0
- package/oidc/src/lib/service/oidc.interaction-policy.d.ts +35 -0
- package/oidc/src/lib/service/oidc.service.d.ts +29 -0
- package/package.json +14 -13
- package/test/index.cjs.js +8 -8
- package/test/index.esm.js +8 -8
- package/test/package.json +11 -11
- package/twilio/package.json +8 -8
- package/zoho/package.json +10 -10
package/mcp/index.esm.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { Injectable, Inject, Optional, Logger, Post, Req, Res, Controller,
|
|
1
|
+
import { Injectable, Inject, Optional, Logger, Post, Req, Res, Get, Delete, Controller, HttpStatus } from '@nestjs/common';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { McpServer } from '@modelcontextprotocol/
|
|
4
|
-
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { McpServer, isLegacyRequest, isJsonContentType, createMcpHandler } from '@modelcontextprotocol/server';
|
|
5
4
|
import { callModelOidcScopeForCallType, resolveEffectiveOidcScopeTerms, inferKeyFromTwoWayFlatFirestoreModelKey, oidcScopeTermsSatisfied } from '@dereekb/firebase';
|
|
6
5
|
import { getOidcScopesFromRequest, OidcProviderConfigService } from '@dereekb/firebase-server/oidc';
|
|
7
6
|
import { makeValuesGroupMap, IMPERSONATION_URL_QUERY_PARAM, performAsyncTasks, authRolesSetHasRoles } from '@dereekb/util';
|
|
8
7
|
import { ModelApiCallModelDispatchService, ModelApiGetService, FirebaseServerStorageService, FirebaseServerEnvService, FirebaseServerAnalyticsService } from '@dereekb/firebase-server';
|
|
9
8
|
import { inspect } from 'node:util';
|
|
10
9
|
import { arktypeToJsonSchemaForExport } from '@dereekb/model';
|
|
11
|
-
import {
|
|
10
|
+
import { toWebRequest, toNodeHandler, NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
|
12
11
|
|
|
13
12
|
function _class_call_check$5(instance, Constructor) {
|
|
14
13
|
if (!(instance instanceof Constructor)) {
|
|
@@ -100,11 +99,12 @@ function _define_property$9(obj, key, value) {
|
|
|
100
99
|
* protected-resource discovery document's `scopes_supported` (RFC 9728 §2).
|
|
101
100
|
*
|
|
102
101
|
* The base list is pulled automatically from the OIDC provider config via the
|
|
103
|
-
* injected {@link OidcProviderConfigService.
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
102
|
+
* injected {@link OidcProviderConfigService.clientRequestableScopesSupported}, so
|
|
103
|
+
* the MCP resource advertises the scopes the issuer grants an arbitrary client
|
|
104
|
+
* without the app restating them. Scopes only an admin-assigned provider profile
|
|
105
|
+
* unlocks are already excluded — requesting one is fatal at the consent unlock
|
|
106
|
+
* gate. Provide this only to narrow the set further; it returns the subset to
|
|
107
|
+
* advertise. When unset, the whole base list is advertised.
|
|
108
108
|
*
|
|
109
109
|
* Advertising these matters because dynamic-registration MCP clients (the Claude
|
|
110
110
|
* Code CLI) read `scopes_supported` to decide which scopes to request on the
|
|
@@ -2036,8 +2036,8 @@ function modelGetToolHandler(args, ctx, deps) {
|
|
|
2036
2036
|
})();
|
|
2037
2037
|
}
|
|
2038
2038
|
function parseModelGetInput(args) {
|
|
2039
|
-
var modelType = args
|
|
2040
|
-
var keys = args
|
|
2039
|
+
var modelType = args['modelType'];
|
|
2040
|
+
var keys = args['keys'];
|
|
2041
2041
|
if (typeof modelType !== 'string' || modelType.length === 0) {
|
|
2042
2042
|
throw new Error('model-get: "modelType" is required and must be a non-empty string.');
|
|
2043
2043
|
}
|
|
@@ -2938,7 +2938,7 @@ var MODEL_ROW_SCHEMA = {
|
|
|
2938
2938
|
}
|
|
2939
2939
|
};
|
|
2940
2940
|
// MCP's `tools/list` validator requires `outputSchema.type === 'object'` at the root
|
|
2941
|
-
// (see
|
|
2941
|
+
// (see the MCP SDK's `ToolSchema`), so the variants are expressed via a top-level
|
|
2942
2942
|
// object whose nested `oneOf` discriminates by the `mode` literal.
|
|
2943
2943
|
var MODEL_INFO_OUTPUT_SCHEMA = {
|
|
2944
2944
|
type: 'object',
|
|
@@ -3740,7 +3740,7 @@ function whoamiToolHandler(args, ctx, deps) {
|
|
|
3740
3740
|
}
|
|
3741
3741
|
// Coerces the `detail` input into a WhoamiDetailLevel, defaulting unknown or absent values to `summary`.
|
|
3742
3742
|
function parseDetailLevel(args) {
|
|
3743
|
-
var value = args
|
|
3743
|
+
var value = args['detail'];
|
|
3744
3744
|
return value === 'claims' || value === 'full' ? value : 'summary';
|
|
3745
3745
|
}
|
|
3746
3746
|
function buildResult(output, text) {
|
|
@@ -4335,30 +4335,30 @@ function urlModelsToolHandler(args, ctx, deps) {
|
|
|
4335
4335
|
})();
|
|
4336
4336
|
}
|
|
4337
4337
|
function parseUrlModelsInput(args) {
|
|
4338
|
-
var url = args
|
|
4338
|
+
var url = args['url'];
|
|
4339
4339
|
if (typeof url !== 'string' || url.length === 0) {
|
|
4340
4340
|
throw new Error('url-models: "url" is required and must be a non-empty string.');
|
|
4341
4341
|
}
|
|
4342
|
-
var keysOnly = args
|
|
4343
|
-
var load = args
|
|
4342
|
+
var keysOnly = args['keysOnly'] === true;
|
|
4343
|
+
var load = args['load'] === true;
|
|
4344
4344
|
if (keysOnly && load) {
|
|
4345
4345
|
throw new Error('url-models: "keysOnly" and "load" cannot be combined — "keysOnly" returns keys without loading documents.');
|
|
4346
4346
|
}
|
|
4347
4347
|
var models;
|
|
4348
|
-
if (args
|
|
4349
|
-
if (!Array.isArray(args
|
|
4348
|
+
if (args['models'] !== undefined) {
|
|
4349
|
+
if (!Array.isArray(args['models']) || args['models'].some(function(m) {
|
|
4350
4350
|
return typeof m !== 'string';
|
|
4351
4351
|
})) {
|
|
4352
4352
|
throw new Error('url-models: "models" must be an array of model-type strings.');
|
|
4353
4353
|
}
|
|
4354
|
-
models = args
|
|
4354
|
+
models = args['models'];
|
|
4355
4355
|
}
|
|
4356
4356
|
var currentUserUid;
|
|
4357
|
-
if (args
|
|
4358
|
-
if (typeof args
|
|
4357
|
+
if (args['currentUserUid'] !== undefined) {
|
|
4358
|
+
if (typeof args['currentUserUid'] !== 'string' || args['currentUserUid'].length === 0) {
|
|
4359
4359
|
throw new Error('url-models: "currentUserUid" must be a non-empty string.');
|
|
4360
4360
|
}
|
|
4361
|
-
currentUserUid = args
|
|
4361
|
+
currentUserUid = args['currentUserUid'];
|
|
4362
4362
|
}
|
|
4363
4363
|
return _object_spread$2(_object_spread_props$1(_object_spread$2({
|
|
4364
4364
|
url: url
|
|
@@ -5306,19 +5306,19 @@ function batchExecuteToolHandler(args, ctx, deps) {
|
|
|
5306
5306
|
// MARK: Input parsing
|
|
5307
5307
|
function parseBatchExecuteInput(args) {
|
|
5308
5308
|
var _args_format, _args_maxParallel;
|
|
5309
|
-
var uploadPath = args
|
|
5309
|
+
var uploadPath = args['uploadPath'];
|
|
5310
5310
|
if (typeof uploadPath !== 'string' || uploadPath.length === 0) {
|
|
5311
5311
|
throw new Error('batch-execute: "uploadPath" is required and must be a non-empty string.');
|
|
5312
5312
|
}
|
|
5313
|
-
var bucketId = args
|
|
5313
|
+
var bucketId = args['bucketId'];
|
|
5314
5314
|
if (bucketId != null && (typeof bucketId !== 'string' || bucketId.length === 0)) {
|
|
5315
5315
|
throw new Error('batch-execute: "bucketId" must be a non-empty string when provided.');
|
|
5316
5316
|
}
|
|
5317
|
-
var format = (_args_format = args
|
|
5317
|
+
var format = (_args_format = args['format']) !== null && _args_format !== void 0 ? _args_format : 'ndjson';
|
|
5318
5318
|
if (format !== 'json' && format !== 'ndjson') {
|
|
5319
5319
|
throw new Error('batch-execute: "format" must be "json" or "ndjson".');
|
|
5320
5320
|
}
|
|
5321
|
-
var maxParallel = (_args_maxParallel = args
|
|
5321
|
+
var maxParallel = (_args_maxParallel = args['maxParallel']) !== null && _args_maxParallel !== void 0 ? _args_maxParallel : DEFAULT_BATCH_EXECUTE_MAX_PARALLEL;
|
|
5322
5322
|
if (typeof maxParallel !== 'number' || !Number.isInteger(maxParallel) || maxParallel < 1) {
|
|
5323
5323
|
throw new Error('batch-execute: "maxParallel" must be a positive integer.');
|
|
5324
5324
|
}
|
|
@@ -5327,8 +5327,8 @@ function parseBatchExecuteInput(args) {
|
|
|
5327
5327
|
bucketId: bucketId !== null && bucketId !== void 0 ? bucketId : undefined,
|
|
5328
5328
|
format: format,
|
|
5329
5329
|
maxParallel: maxParallel,
|
|
5330
|
-
stopOnError: args
|
|
5331
|
-
deleteUploadOnSuccess: args
|
|
5330
|
+
stopOnError: args['stopOnError'] === true,
|
|
5331
|
+
deleteUploadOnSuccess: args['deleteUploadOnSuccess'] !== false
|
|
5332
5332
|
};
|
|
5333
5333
|
}
|
|
5334
5334
|
function readOperationsFile(input) {
|
|
@@ -5409,9 +5409,9 @@ function validateOperation(value, index) {
|
|
|
5409
5409
|
throw new Error("batch-execute: operation[".concat(index, "] must be an object."));
|
|
5410
5410
|
}
|
|
5411
5411
|
var candidate = value;
|
|
5412
|
-
var modelType = candidate
|
|
5413
|
-
var call = candidate
|
|
5414
|
-
var specifier = candidate
|
|
5412
|
+
var modelType = candidate['modelType'];
|
|
5413
|
+
var call = candidate['call'];
|
|
5414
|
+
var specifier = candidate['specifier'];
|
|
5415
5415
|
if (typeof modelType !== 'string' || modelType.length === 0) {
|
|
5416
5416
|
throw new Error("batch-execute: operation[".concat(index, '] is missing a non-empty "modelType".'));
|
|
5417
5417
|
}
|
|
@@ -5425,7 +5425,7 @@ function validateOperation(value, index) {
|
|
|
5425
5425
|
call: call,
|
|
5426
5426
|
modelType: modelType,
|
|
5427
5427
|
specifier: specifier !== null && specifier !== void 0 ? specifier : undefined,
|
|
5428
|
-
data: candidate
|
|
5428
|
+
data: candidate['data']
|
|
5429
5429
|
};
|
|
5430
5430
|
}
|
|
5431
5431
|
function collectPreflightErrors(operations, authorizeOperation) {
|
|
@@ -6053,9 +6053,10 @@ var McpServerFactoryService_1;
|
|
|
6053
6053
|
* Injectable factory that builds {@link McpServer} instances pre-wired to the
|
|
6054
6054
|
* call model dispatch chain.
|
|
6055
6055
|
*
|
|
6056
|
-
* The factory is invoked per Streamable HTTP request
|
|
6057
|
-
*
|
|
6058
|
-
*
|
|
6056
|
+
* The factory is invoked per Streamable HTTP request. That is the shape the MCP
|
|
6057
|
+
* SDK's 2026-07-28 entry (`createMcpHandler`) requires — it builds a fresh server
|
|
6058
|
+
* per request and has no session concept at all — and it is equally what the SDK
|
|
6059
|
+
* recommends for stateless 2025-era serving, so one factory backs both eras.
|
|
6059
6060
|
*/ var McpServerFactoryService = McpServerFactoryService_1 = /*#__PURE__*/ function() {
|
|
6060
6061
|
function McpServerFactoryService(mcpConfig, dispatchService, modelApiGetService, roleReader, analyticsService, storageService) {
|
|
6061
6062
|
_class_call_check$3(this, McpServerFactoryService);
|
|
@@ -6133,7 +6134,7 @@ var McpServerFactoryService_1;
|
|
|
6133
6134
|
batchTool
|
|
6134
6135
|
]);
|
|
6135
6136
|
}
|
|
6136
|
-
server.server.setRequestHandler(
|
|
6137
|
+
server.server.setRequestHandler('tools/list', function() {
|
|
6137
6138
|
return _async_to_generator$2(function() {
|
|
6138
6139
|
var _this;
|
|
6139
6140
|
return _ts_generator$2(this, function(_state) {
|
|
@@ -6141,6 +6142,10 @@ var McpServerFactoryService_1;
|
|
|
6141
6142
|
return [
|
|
6142
6143
|
2,
|
|
6143
6144
|
{
|
|
6145
|
+
// McpToolListEntry types its schemas as opaque `object`: they are read from a manifest file
|
|
6146
|
+
// or produced by arktype's JSON Schema export, so the `type: 'object'` root the SDK's wire
|
|
6147
|
+
// type mandates holds at runtime but cannot be proven statically. Every schema reaching here
|
|
6148
|
+
// either came from arktypeToJsonSchemaForExport or defaulted to `{ type: 'object' }`.
|
|
6144
6149
|
tools: listedTools.map(function(tool) {
|
|
6145
6150
|
return _this._buildToolListEntry(tool, ctx, scopes);
|
|
6146
6151
|
})
|
|
@@ -6149,7 +6154,7 @@ var McpServerFactoryService_1;
|
|
|
6149
6154
|
});
|
|
6150
6155
|
}).call(_this);
|
|
6151
6156
|
});
|
|
6152
|
-
server.server.setRequestHandler(
|
|
6157
|
+
server.server.setRequestHandler('tools/call', function(request) {
|
|
6153
6158
|
return _async_to_generator$2(function() {
|
|
6154
6159
|
return _ts_generator$2(this, function(_state) {
|
|
6155
6160
|
return [
|
|
@@ -7234,29 +7239,168 @@ function _ts_generator$1(thisArg, body) {
|
|
|
7234
7239
|
}
|
|
7235
7240
|
}
|
|
7236
7241
|
/**
|
|
7237
|
-
* Handles a single Streamable HTTP JSON-RPC request
|
|
7242
|
+
* Handles a single Streamable HTTP JSON-RPC request, building a fresh MCP server for it.
|
|
7238
7243
|
*
|
|
7239
|
-
*
|
|
7240
|
-
*
|
|
7241
|
-
*
|
|
7242
|
-
*
|
|
7244
|
+
* Both protocol eras are served, each by its own leg:
|
|
7245
|
+
*
|
|
7246
|
+
* - **2026-07-28 (modern)** — `createMcpHandler`, which is per-request and stateless by
|
|
7247
|
+
* construction: there is no `Mcp-Session-Id` and no transport bookkeeping.
|
|
7248
|
+
* - **2025-era (legacy)** — a `NodeStreamableHTTPServerTransport` in stateless mode, which is
|
|
7249
|
+
* byte-for-byte what this endpoint served before the SDK v2 upgrade.
|
|
7250
|
+
*
|
|
7251
|
+
* The legacy leg is wired by hand (the SDK's documented `isLegacyRequest` composition) rather
|
|
7252
|
+
* than left to `createMcpHandler`'s built-in `legacy: 'stateless'` fallback, because that
|
|
7253
|
+
* fallback builds its transport with `sessionIdGenerator` alone and therefore answers in
|
|
7254
|
+
* `text/event-stream`. See {@link handleLegacyMcpRequest} for why that framing is not viable
|
|
7255
|
+
* here.
|
|
7243
7256
|
*
|
|
7244
7257
|
* The caller is expected to have already validated the bearer token (via the
|
|
7245
7258
|
* OIDC bearer middleware) before this function runs.
|
|
7246
7259
|
*
|
|
7247
7260
|
* @param req - The Express request carrying the JSON-RPC body.
|
|
7248
7261
|
* @param res - The Express response.
|
|
7262
|
+
* @param buildServer - Factory producing the MCP server that backs this request.
|
|
7263
|
+
*/ function handleStreamableHttpMcpRequest(req, res, buildServer) {
|
|
7264
|
+
return _async_to_generator$1(function() {
|
|
7265
|
+
var probe;
|
|
7266
|
+
return _ts_generator$1(this, function(_state) {
|
|
7267
|
+
switch(_state.label){
|
|
7268
|
+
case 0:
|
|
7269
|
+
// Hand-wired compositions must reject non-JSON POST bodies themselves — neither the modern
|
|
7270
|
+
// entry's routing nor the legacy transport does it on our behalf once we classify up front.
|
|
7271
|
+
if (!isJsonContentType(req.headers['content-type'])) {
|
|
7272
|
+
res.status(415).json({
|
|
7273
|
+
statusCode: 415,
|
|
7274
|
+
message: 'Unsupported Media Type'
|
|
7275
|
+
});
|
|
7276
|
+
return [
|
|
7277
|
+
2
|
|
7278
|
+
];
|
|
7279
|
+
}
|
|
7280
|
+
return [
|
|
7281
|
+
4,
|
|
7282
|
+
toWebRequest(req, req.body)
|
|
7283
|
+
];
|
|
7284
|
+
case 1:
|
|
7285
|
+
probe = _state.sent();
|
|
7286
|
+
return [
|
|
7287
|
+
4,
|
|
7288
|
+
isLegacyRequest(probe)
|
|
7289
|
+
];
|
|
7290
|
+
case 2:
|
|
7291
|
+
if (!_state.sent()) return [
|
|
7292
|
+
3,
|
|
7293
|
+
4
|
|
7294
|
+
];
|
|
7295
|
+
return [
|
|
7296
|
+
4,
|
|
7297
|
+
handleLegacyMcpRequest(req, res, buildServer())
|
|
7298
|
+
];
|
|
7299
|
+
case 3:
|
|
7300
|
+
_state.sent();
|
|
7301
|
+
return [
|
|
7302
|
+
3,
|
|
7303
|
+
6
|
|
7304
|
+
];
|
|
7305
|
+
case 4:
|
|
7306
|
+
return [
|
|
7307
|
+
4,
|
|
7308
|
+
handleModernMcpRequest(req, res, buildServer)
|
|
7309
|
+
];
|
|
7310
|
+
case 5:
|
|
7311
|
+
_state.sent();
|
|
7312
|
+
_state.label = 6;
|
|
7313
|
+
case 6:
|
|
7314
|
+
return [
|
|
7315
|
+
2
|
|
7316
|
+
];
|
|
7317
|
+
}
|
|
7318
|
+
});
|
|
7319
|
+
})();
|
|
7320
|
+
}
|
|
7321
|
+
/**
|
|
7322
|
+
* Serves one 2026-07-28 request through `createMcpHandler`.
|
|
7323
|
+
*
|
|
7324
|
+
* `legacy: 'reject'` keeps this leg modern-only — 2025-era traffic never reaches it, having
|
|
7325
|
+
* already been routed to {@link handleLegacyMcpRequest}.
|
|
7326
|
+
*
|
|
7327
|
+
* `responseMode: 'json'` is the modern-leg equivalent of the legacy transport's
|
|
7328
|
+
* `enableJsonResponse`, keeping the endpoint on a single `application/json` body. Safe here
|
|
7329
|
+
* because no tool handler emits mid-call notifications, which are the only thing `'json'` drops.
|
|
7330
|
+
*
|
|
7331
|
+
* @param req - The Express request carrying the JSON-RPC body.
|
|
7332
|
+
* @param res - The Express response.
|
|
7333
|
+
* @param buildServer - Factory producing the MCP server that backs this request.
|
|
7334
|
+
*/ function handleModernMcpRequest(req, res, buildServer) {
|
|
7335
|
+
return _async_to_generator$1(function() {
|
|
7336
|
+
var handler;
|
|
7337
|
+
return _ts_generator$1(this, function(_state) {
|
|
7338
|
+
switch(_state.label){
|
|
7339
|
+
case 0:
|
|
7340
|
+
handler = createMcpHandler(buildServer, {
|
|
7341
|
+
legacy: 'reject',
|
|
7342
|
+
responseMode: 'json'
|
|
7343
|
+
});
|
|
7344
|
+
_state.label = 1;
|
|
7345
|
+
case 1:
|
|
7346
|
+
_state.trys.push([
|
|
7347
|
+
1,
|
|
7348
|
+
,
|
|
7349
|
+
3,
|
|
7350
|
+
5
|
|
7351
|
+
]);
|
|
7352
|
+
return [
|
|
7353
|
+
4,
|
|
7354
|
+
toNodeHandler(handler)(req, res, req.body)
|
|
7355
|
+
];
|
|
7356
|
+
case 2:
|
|
7357
|
+
_state.sent();
|
|
7358
|
+
return [
|
|
7359
|
+
3,
|
|
7360
|
+
5
|
|
7361
|
+
];
|
|
7362
|
+
case 3:
|
|
7363
|
+
return [
|
|
7364
|
+
4,
|
|
7365
|
+
handler.close()
|
|
7366
|
+
];
|
|
7367
|
+
case 4:
|
|
7368
|
+
_state.sent();
|
|
7369
|
+
return [
|
|
7370
|
+
7
|
|
7371
|
+
];
|
|
7372
|
+
case 5:
|
|
7373
|
+
return [
|
|
7374
|
+
2
|
|
7375
|
+
];
|
|
7376
|
+
}
|
|
7377
|
+
});
|
|
7378
|
+
})();
|
|
7379
|
+
}
|
|
7380
|
+
/**
|
|
7381
|
+
* Serves one 2025-era request through a stateless `NodeStreamableHTTPServerTransport`.
|
|
7382
|
+
*
|
|
7383
|
+
* `sessionIdGenerator: undefined` selects stateless mode: the transport binds the server,
|
|
7384
|
+
* processes one request, and is discarded.
|
|
7385
|
+
*
|
|
7386
|
+
* `enableJsonResponse: true` replies with a single `application/json` body instead of framing it
|
|
7387
|
+
* as `text/event-stream`. Stateless mode answers exactly one JSON-RPC message per request and the
|
|
7388
|
+
* controller only implements POST (there is no standalone SSE stream to keep open), so SSE framing
|
|
7389
|
+
* adds nothing while making the endpoint fragile behind proxies that buffer or rewrite streamed
|
|
7390
|
+
* responses — the failure that originally pushed the demo app off its dev proxy.
|
|
7391
|
+
*
|
|
7392
|
+
* @param req - The Express request carrying the JSON-RPC body.
|
|
7393
|
+
* @param res - The Express response.
|
|
7249
7394
|
* @param server - The MCP server instance to back the transport.
|
|
7250
|
-
|
|
7251
|
-
*/ function handleStreamableHttpMcpRequest(req, res, server) {
|
|
7395
|
+
*/ function handleLegacyMcpRequest(req, res, server) {
|
|
7252
7396
|
return _async_to_generator$1(function() {
|
|
7253
7397
|
var transport;
|
|
7254
7398
|
return _ts_generator$1(this, function(_state) {
|
|
7255
7399
|
switch(_state.label){
|
|
7256
7400
|
case 0:
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7401
|
+
transport = new NodeStreamableHTTPServerTransport({
|
|
7402
|
+
sessionIdGenerator: undefined,
|
|
7403
|
+
enableJsonResponse: true
|
|
7260
7404
|
});
|
|
7261
7405
|
res.on('close', function() {
|
|
7262
7406
|
void transport.close();
|
|
@@ -7448,9 +7592,11 @@ var McpController_1;
|
|
|
7448
7592
|
* which must include `'/mcp'` in its `protectedPaths`. By the time the request reaches
|
|
7449
7593
|
* this controller, `req.auth` is populated with the authenticated user's data.
|
|
7450
7594
|
*
|
|
7451
|
-
* Each request gets a fresh
|
|
7452
|
-
*
|
|
7453
|
-
*
|
|
7595
|
+
* Each request gets a fresh MCP server (stateless mode), which is adequate for Claude
|
|
7596
|
+
* custom-connector style usage. Both protocol eras are served — 2026-07-28 through the
|
|
7597
|
+
* SDK's per-request `createMcpHandler` entry, and 2025-era through a stateless Streamable
|
|
7598
|
+
* HTTP transport. A session-tracked variant can be layered on later if streaming tool
|
|
7599
|
+
* output becomes a requirement.
|
|
7454
7600
|
*/ var McpController = McpController_1 = /*#__PURE__*/ function() {
|
|
7455
7601
|
function McpController(factory) {
|
|
7456
7602
|
_class_call_check$2(this, McpController);
|
|
@@ -7463,15 +7609,12 @@ var McpController_1;
|
|
|
7463
7609
|
key: "handleMcpRequest",
|
|
7464
7610
|
value: function handleMcpRequest(req, res) {
|
|
7465
7611
|
return _async_to_generator(function() {
|
|
7466
|
-
var
|
|
7612
|
+
var _this, auth, error;
|
|
7467
7613
|
return _ts_generator(this, function(_state) {
|
|
7468
7614
|
switch(_state.label){
|
|
7469
7615
|
case 0:
|
|
7616
|
+
_this = this;
|
|
7470
7617
|
auth = req.auth;
|
|
7471
|
-
server = this.factory.createServer({
|
|
7472
|
-
auth: auth,
|
|
7473
|
-
rawRequest: req
|
|
7474
|
-
});
|
|
7475
7618
|
_state.label = 1;
|
|
7476
7619
|
case 1:
|
|
7477
7620
|
_state.trys.push([
|
|
@@ -7482,7 +7625,12 @@ var McpController_1;
|
|
|
7482
7625
|
]);
|
|
7483
7626
|
return [
|
|
7484
7627
|
4,
|
|
7485
|
-
handleStreamableHttpMcpRequest(req, res,
|
|
7628
|
+
handleStreamableHttpMcpRequest(req, res, function() {
|
|
7629
|
+
return _this.factory.createServer({
|
|
7630
|
+
auth: auth,
|
|
7631
|
+
rawRequest: req
|
|
7632
|
+
});
|
|
7633
|
+
})
|
|
7486
7634
|
];
|
|
7487
7635
|
case 2:
|
|
7488
7636
|
_state.sent();
|
|
@@ -7511,6 +7659,45 @@ var McpController_1;
|
|
|
7511
7659
|
});
|
|
7512
7660
|
}).call(this);
|
|
7513
7661
|
}
|
|
7662
|
+
},
|
|
7663
|
+
{
|
|
7664
|
+
/**
|
|
7665
|
+
* Rejects the Streamable HTTP transport's optional `GET` method, which opens a standalone SSE
|
|
7666
|
+
* stream. Not applicable in stateless mode. Separate from {@link handleUnsupportedDelete} because
|
|
7667
|
+
* NestJS binds one HTTP method per handler — stacking route decorators would silently drop one.
|
|
7668
|
+
*
|
|
7669
|
+
* @param res - The Express response to write the rejection to.
|
|
7670
|
+
*/ key: "handleUnsupportedGet",
|
|
7671
|
+
value: function handleUnsupportedGet(res) {
|
|
7672
|
+
this._rejectUnsupportedMethod(res);
|
|
7673
|
+
}
|
|
7674
|
+
},
|
|
7675
|
+
{
|
|
7676
|
+
/**
|
|
7677
|
+
* Rejects the Streamable HTTP transport's optional `DELETE` method, which tears down a session.
|
|
7678
|
+
* Not applicable in stateless mode — no session is ever issued.
|
|
7679
|
+
*
|
|
7680
|
+
* @param res - The Express response to write the rejection to.
|
|
7681
|
+
*/ key: "handleUnsupportedDelete",
|
|
7682
|
+
value: function handleUnsupportedDelete(res) {
|
|
7683
|
+
this._rejectUnsupportedMethod(res);
|
|
7684
|
+
}
|
|
7685
|
+
},
|
|
7686
|
+
{
|
|
7687
|
+
/**
|
|
7688
|
+
* Answers with a spec-conformant `405 Method Not Allowed` + `Allow` header. Without these
|
|
7689
|
+
* handlers NestJS answers `404`, which some clients treat as a hard failure rather than
|
|
7690
|
+
* "the server doesn't offer this".
|
|
7691
|
+
*
|
|
7692
|
+
* @param res - The Express response to write the rejection to.
|
|
7693
|
+
*/ key: "_rejectUnsupportedMethod",
|
|
7694
|
+
value: function _rejectUnsupportedMethod(res) {
|
|
7695
|
+
res.setHeader('Allow', 'POST');
|
|
7696
|
+
res.status(HttpStatus.METHOD_NOT_ALLOWED).json({
|
|
7697
|
+
statusCode: HttpStatus.METHOD_NOT_ALLOWED,
|
|
7698
|
+
message: 'Method Not Allowed'
|
|
7699
|
+
});
|
|
7700
|
+
}
|
|
7514
7701
|
}
|
|
7515
7702
|
]);
|
|
7516
7703
|
return McpController;
|
|
@@ -7520,6 +7707,14 @@ __decorate([
|
|
|
7520
7707
|
__param(0, Req()),
|
|
7521
7708
|
__param(1, Res())
|
|
7522
7709
|
], McpController.prototype, "handleMcpRequest", null);
|
|
7710
|
+
__decorate([
|
|
7711
|
+
Get(),
|
|
7712
|
+
__param(0, Res())
|
|
7713
|
+
], McpController.prototype, "handleUnsupportedGet", null);
|
|
7714
|
+
__decorate([
|
|
7715
|
+
Delete(),
|
|
7716
|
+
__param(0, Res())
|
|
7717
|
+
], McpController.prototype, "handleUnsupportedDelete", null);
|
|
7523
7718
|
McpController = McpController_1 = __decorate([
|
|
7524
7719
|
Controller('mcp'),
|
|
7525
7720
|
__param(0, Inject(McpServerFactoryService))
|
|
@@ -7605,6 +7800,13 @@ function _unsupported_iterable_to_array$2(o, minLen) {
|
|
|
7605
7800
|
* URIs must live at the host root. Apps need to exclude `.well-known/{*path}`
|
|
7606
7801
|
* from any global API route prefix (see `FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE`
|
|
7607
7802
|
* in `firebase-server/oidc` for the corresponding OIDC excludes).
|
|
7803
|
+
*
|
|
7804
|
+
* Because the document must be reachable at the origin root, the MCP endpoint has to be
|
|
7805
|
+
* exposed through a host that serves the app at `/` — Firebase Hosting, or the hosting
|
|
7806
|
+
* emulator in development. Pointing a client straight at the Functions emulator origin
|
|
7807
|
+
* (`http://localhost:<port>/<project>/<region>/<function>/mcp`) breaks discovery: that
|
|
7808
|
+
* runtime only routes `/<project>/<region>/<function>/…`, so nothing answers at the root
|
|
7809
|
+
* and a client that has not yet seen a 401 challenge cannot find the issuer.
|
|
7608
7810
|
*/ var McpWellKnownController = /*#__PURE__*/ function() {
|
|
7609
7811
|
function McpWellKnownController(mcpConfig, oidcProviderConfigService) {
|
|
7610
7812
|
_class_call_check$1(this, McpWellKnownController);
|
|
@@ -7615,12 +7817,26 @@ function _unsupported_iterable_to_array$2(o, minLen) {
|
|
|
7615
7817
|
}
|
|
7616
7818
|
_create_class$1(McpWellKnownController, [
|
|
7617
7819
|
{
|
|
7618
|
-
|
|
7820
|
+
/**
|
|
7821
|
+
* Serves both RFC 9728 discovery URLs — the primary form (§3.1), which inserts the resource's
|
|
7822
|
+
* path after the well-known prefix (`/.well-known/oauth-protected-resource/mcp`), and the bare
|
|
7823
|
+
* form (`/.well-known/oauth-protected-resource`) that clients probe as a fallback.
|
|
7824
|
+
*
|
|
7825
|
+
* The document is identical either way; the resource identity comes from
|
|
7826
|
+
* {@link McpModuleConfig.mcpUrl}, not from the request path. Serving the primary form means
|
|
7827
|
+
* discovery succeeds on a client's first probe rather than depending on it implementing the
|
|
7828
|
+
* fallback.
|
|
7829
|
+
*
|
|
7830
|
+
* @returns The protected-resource metadata document.
|
|
7831
|
+
*/ key: "getProtectedResourceMetadata",
|
|
7619
7832
|
value: function getProtectedResourceMetadata() {
|
|
7620
7833
|
var _this_mcpConfig = this.mcpConfig, mcpUrl = _this_mcpConfig.mcpUrl, oidcIssuer = _this_mcpConfig.oidcIssuer, scopesFilter = _this_mcpConfig.scopesSupported;
|
|
7621
|
-
// Base scope list is the
|
|
7622
|
-
//
|
|
7623
|
-
|
|
7834
|
+
// Base scope list is what the issuer grants an arbitrary client, NOT its full
|
|
7835
|
+
// `scopes_supported`: a dynamic-registration client copies this list verbatim onto
|
|
7836
|
+
// `/authorize`, and a provider-profile-gated scope in it would be hard-rejected at the
|
|
7837
|
+
// consent unlock gate (`access_denied`) for every client lacking that profile assignment.
|
|
7838
|
+
// The optional filter narrows the list further.
|
|
7839
|
+
var providerScopes = this.oidcProviderConfigService.clientRequestableScopesSupported;
|
|
7624
7840
|
var scopes = scopesFilter ? scopesFilter(providerScopes) : providerScopes;
|
|
7625
7841
|
return _object_spread({
|
|
7626
7842
|
resource: mcpUrl,
|
|
@@ -7636,7 +7852,10 @@ function _unsupported_iterable_to_array$2(o, minLen) {
|
|
|
7636
7852
|
return McpWellKnownController;
|
|
7637
7853
|
}();
|
|
7638
7854
|
__decorate([
|
|
7639
|
-
Get(
|
|
7855
|
+
Get([
|
|
7856
|
+
'oauth-protected-resource',
|
|
7857
|
+
'oauth-protected-resource/{*path}'
|
|
7858
|
+
])
|
|
7640
7859
|
], McpWellKnownController.prototype, "getProtectedResourceMetadata", null);
|
|
7641
7860
|
McpWellKnownController = __decorate([
|
|
7642
7861
|
Controller('.well-known'),
|
package/mcp/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/firebase-server/mcp",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.37.0",
|
|
4
4
|
"peerDependencies": {
|
|
5
|
-
"@dereekb/analytics": "13.
|
|
6
|
-
"@dereekb/date": "13.
|
|
7
|
-
"@dereekb/firebase": "13.
|
|
8
|
-
"@dereekb/firebase-server": "13.
|
|
9
|
-
"@dereekb/firebase-server/oidc": "13.
|
|
10
|
-
"@dereekb/model": "13.
|
|
11
|
-
"@dereekb/nestjs": "13.
|
|
12
|
-
"@dereekb/rxjs": "13.
|
|
13
|
-
"@dereekb/util": "13.
|
|
14
|
-
"@dereekb/zoho": "13.
|
|
15
|
-
"@modelcontextprotocol/
|
|
5
|
+
"@dereekb/analytics": "13.37.0",
|
|
6
|
+
"@dereekb/date": "13.37.0",
|
|
7
|
+
"@dereekb/firebase": "13.37.0",
|
|
8
|
+
"@dereekb/firebase-server": "13.37.0",
|
|
9
|
+
"@dereekb/firebase-server/oidc": "13.37.0",
|
|
10
|
+
"@dereekb/model": "13.37.0",
|
|
11
|
+
"@dereekb/nestjs": "13.37.0",
|
|
12
|
+
"@dereekb/rxjs": "13.37.0",
|
|
13
|
+
"@dereekb/util": "13.37.0",
|
|
14
|
+
"@dereekb/zoho": "13.37.0",
|
|
15
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
16
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
16
17
|
"@nestjs/common": "^11.1.19",
|
|
17
18
|
"@nestjs/core": "^11.1.19",
|
|
18
19
|
"arktype": "^2.2.0",
|
|
@@ -7,13 +7,38 @@ import { McpServerFactoryService } from '../service/mcp.server.factory';
|
|
|
7
7
|
* which must include `'/mcp'` in its `protectedPaths`. By the time the request reaches
|
|
8
8
|
* this controller, `req.auth` is populated with the authenticated user's data.
|
|
9
9
|
*
|
|
10
|
-
* Each request gets a fresh
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Each request gets a fresh MCP server (stateless mode), which is adequate for Claude
|
|
11
|
+
* custom-connector style usage. Both protocol eras are served — 2026-07-28 through the
|
|
12
|
+
* SDK's per-request `createMcpHandler` entry, and 2025-era through a stateless Streamable
|
|
13
|
+
* HTTP transport. A session-tracked variant can be layered on later if streaming tool
|
|
14
|
+
* output becomes a requirement.
|
|
13
15
|
*/
|
|
14
16
|
export declare class McpController {
|
|
15
17
|
private readonly factory;
|
|
16
18
|
private readonly _logger;
|
|
17
19
|
constructor(factory: McpServerFactoryService);
|
|
18
20
|
handleMcpRequest(req: Request, res: Response): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Rejects the Streamable HTTP transport's optional `GET` method, which opens a standalone SSE
|
|
23
|
+
* stream. Not applicable in stateless mode. Separate from {@link handleUnsupportedDelete} because
|
|
24
|
+
* NestJS binds one HTTP method per handler — stacking route decorators would silently drop one.
|
|
25
|
+
*
|
|
26
|
+
* @param res - The Express response to write the rejection to.
|
|
27
|
+
*/
|
|
28
|
+
handleUnsupportedGet(res: Response): void;
|
|
29
|
+
/**
|
|
30
|
+
* Rejects the Streamable HTTP transport's optional `DELETE` method, which tears down a session.
|
|
31
|
+
* Not applicable in stateless mode — no session is ever issued.
|
|
32
|
+
*
|
|
33
|
+
* @param res - The Express response to write the rejection to.
|
|
34
|
+
*/
|
|
35
|
+
handleUnsupportedDelete(res: Response): void;
|
|
36
|
+
/**
|
|
37
|
+
* Answers with a spec-conformant `405 Method Not Allowed` + `Allow` header. Without these
|
|
38
|
+
* handlers NestJS answers `404`, which some clients treat as a hard failure rather than
|
|
39
|
+
* "the server doesn't offer this".
|
|
40
|
+
*
|
|
41
|
+
* @param res - The Express response to write the rejection to.
|
|
42
|
+
*/
|
|
43
|
+
private _rejectUnsupportedMethod;
|
|
19
44
|
}
|