@hachej/boring-mcp 0.1.78 → 0.1.79

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.
@@ -2,6 +2,8 @@ import * as _hachej_boring_workspace from '@hachej/boring-workspace';
2
2
  import { ToolExecContext, AgentTool } from '@hachej/boring-workspace';
3
3
  import { McpActor, McpProviderId, McpToolCatalogEntry, McpToolSearchResult, McpToolDescribeResult, McpSourceRegistry, McpTransportClient, McpProviderTemplate, McpDiscoveredTool, McpReadonlyCallAuditEvent, McpReadonlyCallInput, McpReadonlyCallResult, McpSourceDto, McpSourceStatusPayload, McpDoctorResult, McpProbeResult, McpSource, McpErrorCode, McpConnectorRef, McpSourceStatus, McpDiscoveredResource } from '../shared/index.js';
4
4
  export { AIRTABLE_MCP_TEMPLATE, BORING_MCP_PLUGIN_ID, BORING_MCP_SOURCES_PANEL_ID, BORING_MCP_SOURCES_TAB_PANEL_ID, DEFAULT_MCP_PROVIDER_TEMPLATES, MCP_ERROR_CODES, MCP_TOOL_NAME_PATTERN, McpAccessFacade, McpCredentialProvider, McpDoctorIssue, McpError, McpSourceAccessPolicy, McpSourceOwnerKind, McpSourceStore, McpToolCallResult, McpToolDecision, McpToolRisk, McpTransport, McpTransportListToolsOptions, NOTION_MCP_TEMPLATE, NormalizedMcpTool, assertMcpToolAllowed, classifyMcpTool, classifyMcpTools, containsMcpCanary, containsMcpSecret, containsMcpSecretOrCanary, doctorMcpSource, getMcpProviderTemplate, redactMcpSecrets, toMcpSourceDto, validateMcpToolName } from '../shared/index.js';
5
+ import { FastifyRequest } from 'fastify';
6
+ import { CoreWorkspaceAgentServer } from '@hachej/boring-core/app/server';
5
7
 
6
8
  interface McpToolCatalogSnapshot {
7
9
  sourceId: string;
@@ -294,6 +296,77 @@ declare function createManagedConnectorSourceId(actor: McpActor, provider: McpPr
294
296
  declare function createLegacyManagedConnectorSourceId(actor: McpActor, provider: McpProviderId): string;
295
297
  declare function createManagedConnectorAdapter(options: ManagedConnectorAdapterOptions): ManagedConnectorAdapter;
296
298
 
299
+ /** App server surface the boring-mcp binding depends on. */
300
+ type BoringMcpAppServer = CoreWorkspaceAgentServer;
301
+ /**
302
+ * Per-deployment configuration for the boring-mcp server binding. Everything a
303
+ * host used to hardcode when copying the glue lives here.
304
+ */
305
+ interface BoringMcpBindingConfig {
306
+ /** Managed connector providers this deployment exposes. */
307
+ connectorConfigs: readonly ManagedConnectorConfig[];
308
+ /**
309
+ * Env var names checked (in order) for the Composio managed-connector API
310
+ * key. Defaults to `['COMPOSIO_API_KEY']`.
311
+ */
312
+ secretEnvVars?: readonly string[];
313
+ /** Redaction canary tokens forwarded to the managed connector adapter. */
314
+ redactionCanaries?: readonly string[];
315
+ /** Composio MCP transport client identity. */
316
+ clientName?: string;
317
+ clientVersion?: string;
318
+ }
319
+ interface BoringMcpServerRuntimeConfig {
320
+ enabled: boolean;
321
+ composioApiKeyConfigured: boolean;
322
+ maxReadonlyInputBytes: number;
323
+ }
324
+ declare function readBoringMcpServerConfig(env?: NodeJS.ProcessEnv, options?: {
325
+ secretEnvVars?: readonly string[];
326
+ }): BoringMcpServerRuntimeConfig;
327
+ interface CreateManagedConnectorSecretResolverOptions {
328
+ env?: NodeJS.ProcessEnv;
329
+ configs: readonly ManagedConnectorConfig[];
330
+ secretEnvVars?: readonly string[];
331
+ }
332
+ declare function createManagedConnectorSecretResolver(options: CreateManagedConnectorSecretResolverOptions): ManagedConnectorSecretResolver;
333
+ interface CreateManagedConnectorAdapterOptions {
334
+ config: BoringMcpBindingConfig;
335
+ registry: ManagedConnectorSourceRegistry;
336
+ env?: NodeJS.ProcessEnv;
337
+ provider?: ManagedConnectorProvider;
338
+ }
339
+ declare function createBoringMcpManagedConnectorAdapter(options: CreateManagedConnectorAdapterOptions): ManagedConnectorAdapter;
340
+ declare function boringMcpAgentSessionNamespace(ctx: {
341
+ workspaceId: string;
342
+ request?: FastifyRequest;
343
+ userId?: string;
344
+ }): string;
345
+ declare function createUserSettingsMcpSourceRegistry(app: Pick<BoringMcpAppServer, 'userStore' | 'config'>, actorScope: McpActor): ManagedConnectorSourceRegistry;
346
+ interface CreateBoringMcpAppAgentToolsOptions {
347
+ config: BoringMcpBindingConfig;
348
+ env?: NodeJS.ProcessEnv;
349
+ transport?: McpTransportClient;
350
+ }
351
+ declare function createBoringMcpAppAgentTools(app: Pick<BoringMcpAppServer, 'userStore' | 'config'>, actor: McpActor, options: CreateBoringMcpAppAgentToolsOptions): AgentTool[];
352
+ declare function createBoringMcpAppAgentToolsForRequest(app: Pick<BoringMcpAppServer, 'userStore' | 'config'>, ctx: {
353
+ workspaceId: string;
354
+ authSubject?: string;
355
+ }, options: CreateBoringMcpAppAgentToolsOptions): AgentTool[];
356
+ interface RegisterBoringMcpRoutesOptions {
357
+ config: BoringMcpBindingConfig;
358
+ provider?: ManagedConnectorProvider;
359
+ env?: NodeJS.ProcessEnv;
360
+ transport?: McpTransportClient;
361
+ /**
362
+ * Behavior when boring-mcp is disabled for this deployment.
363
+ * - `skip` (default): do not register the routes at all (client sees 404).
364
+ * - `serve-503`: always register; handlers reject with 503 while disabled.
365
+ */
366
+ whenDisabled?: 'skip' | 'serve-503';
367
+ }
368
+ declare function registerBoringMcpRoutes(app: BoringMcpAppServer, options: RegisterBoringMcpRoutesOptions): void;
369
+
297
370
  interface ComposioMcpSession {
298
371
  id: string;
299
372
  mcp: {
@@ -371,4 +444,4 @@ declare const _default: {
371
444
  agentTools: _hachej_boring_workspace.AgentTool[] | undefined;
372
445
  };
373
446
 
374
- export { BORING_MCP_AGENT_BRIDGE_TOOL_DEFINITIONS, BORING_MCP_AGENT_BRIDGE_TOOL_NAMES, type BoringMcpAgentBridgeContext, type BoringMcpAgentBridgeRegistry, type BoringMcpAgentBridgeTool, type BoringMcpAgentBridgeToolDefinition, type BoringMcpAgentBridgeToolName, type BoringMcpAgentToolActorResolver, type BoringMcpLaunchGateInput, type BoringMcpLaunchGateIssue, type BoringMcpLaunchGateIssueCode, type BoringMcpLaunchGateResult, type BoringMcpReadonlyCallOptions, type BoringMcpReadonlyCaller, type BoringMcpSourceHandlers, type BoringMcpSourceHandlersOptions, type BoringMcpToolCatalog, type BoringMcpToolCatalogOptions, type ComposioManagedConnectorProviderOptions, type ComposioMcpSession, type CreateBoringMcpAgentToolsOptions, type CreateBoringMcpServerPluginOptions, InMemoryMcpRateBudgetGate, type InMemoryMcpRateBudgetGateOptions, InMemoryMcpToolCatalogCache, type ManagedConnectorAcceptedGap, type ManagedConnectorAdapter, type ManagedConnectorAdapterOptions, type ManagedConnectorConfig, type ManagedConnectorPreflightCheck, type ManagedConnectorPreflightEvidence, type ManagedConnectorPreflightResult, type ManagedConnectorProbeResponse, type ManagedConnectorProvider, type ManagedConnectorRiskStatus, type ManagedConnectorSecret, type ManagedConnectorSecretResolver, type ManagedConnectorSecretStorage, type ManagedConnectorSourceRegistry, type ManagedConnectorStartInput, type ManagedConnectorStartResponse, type ManagedConnectorStartResult, type ManagedConnectorStatusResponse, type ManagedConnectorVendorRiskEvidence, McpActor, McpConnectorRef, McpDiscoveredResource, McpDiscoveredTool, McpDoctorResult, McpErrorCode, McpProbeResult, type McpProviderCallContext, type McpProviderHardeningOptions, McpProviderId, type McpProviderOperation, type McpProviderRateBudgetGate, McpProviderTemplate, McpReadonlyCallAuditEvent, type McpReadonlyCallAuditSink, McpReadonlyCallInput, McpReadonlyCallResult, type McpSdkEndpoint, type McpSdkEndpointResolverInput, type McpSdkTransportOptions, McpSource, McpSourceDto, McpSourceRegistry, McpSourceStatus, McpSourceStatusPayload, type McpToolCatalogCache, McpToolCatalogEntry, type McpToolDescribeInput, McpToolDescribeResult, McpToolSearchResult, type McpToolsSearchInput, McpTransportClient, type ResolveComposioMcpSessionInput, assertManagedConnectorPreflight, assertMcpPublicPayloadSecretFree, createBoringMcpAgentBridgeRegistry, createBoringMcpAgentTools, createBoringMcpReadonlyCaller, createBoringMcpServerPlugin, createBoringMcpSourceHandlers, createBoringMcpToolCatalog, createComposioManagedConnectorProvider, createComposioMcpTransport, createHardenedMcpTransport, createLegacyManagedConnectorSourceId, createManagedConnectorAdapter, createManagedConnectorSourceId, createMcpSchemaHash, createMcpSdkStreamableHttpTransport, createMcpSourceStatusPayload, _default as default, evaluateBoringMcpLaunchGate, isActorOwnedMcpSource, listBoringMcpAgentBridgeTools, normalizeMcpCatalogTool, requireActorOwnedMcpSource, resolveComposioMcpSession, validateManagedConnectorPreflight, validateMcpSourceId, verifyMcpDisconnectResult };
447
+ export { BORING_MCP_AGENT_BRIDGE_TOOL_DEFINITIONS, BORING_MCP_AGENT_BRIDGE_TOOL_NAMES, type BoringMcpAgentBridgeContext, type BoringMcpAgentBridgeRegistry, type BoringMcpAgentBridgeTool, type BoringMcpAgentBridgeToolDefinition, type BoringMcpAgentBridgeToolName, type BoringMcpAgentToolActorResolver, type BoringMcpAppServer, type BoringMcpBindingConfig, type BoringMcpLaunchGateInput, type BoringMcpLaunchGateIssue, type BoringMcpLaunchGateIssueCode, type BoringMcpLaunchGateResult, type BoringMcpReadonlyCallOptions, type BoringMcpReadonlyCaller, type BoringMcpServerRuntimeConfig, type BoringMcpSourceHandlers, type BoringMcpSourceHandlersOptions, type BoringMcpToolCatalog, type BoringMcpToolCatalogOptions, type ComposioManagedConnectorProviderOptions, type ComposioMcpSession, type CreateBoringMcpAgentToolsOptions, type CreateBoringMcpAppAgentToolsOptions, type CreateBoringMcpServerPluginOptions, type CreateManagedConnectorAdapterOptions, type CreateManagedConnectorSecretResolverOptions, InMemoryMcpRateBudgetGate, type InMemoryMcpRateBudgetGateOptions, InMemoryMcpToolCatalogCache, type ManagedConnectorAcceptedGap, type ManagedConnectorAdapter, type ManagedConnectorAdapterOptions, type ManagedConnectorConfig, type ManagedConnectorPreflightCheck, type ManagedConnectorPreflightEvidence, type ManagedConnectorPreflightResult, type ManagedConnectorProbeResponse, type ManagedConnectorProvider, type ManagedConnectorRiskStatus, type ManagedConnectorSecret, type ManagedConnectorSecretResolver, type ManagedConnectorSecretStorage, type ManagedConnectorSourceRegistry, type ManagedConnectorStartInput, type ManagedConnectorStartResponse, type ManagedConnectorStartResult, type ManagedConnectorStatusResponse, type ManagedConnectorVendorRiskEvidence, McpActor, McpConnectorRef, McpDiscoveredResource, McpDiscoveredTool, McpDoctorResult, McpErrorCode, McpProbeResult, type McpProviderCallContext, type McpProviderHardeningOptions, McpProviderId, type McpProviderOperation, type McpProviderRateBudgetGate, McpProviderTemplate, McpReadonlyCallAuditEvent, type McpReadonlyCallAuditSink, McpReadonlyCallInput, McpReadonlyCallResult, type McpSdkEndpoint, type McpSdkEndpointResolverInput, type McpSdkTransportOptions, McpSource, McpSourceDto, McpSourceRegistry, McpSourceStatus, McpSourceStatusPayload, type McpToolCatalogCache, McpToolCatalogEntry, type McpToolDescribeInput, McpToolDescribeResult, McpToolSearchResult, type McpToolsSearchInput, McpTransportClient, type RegisterBoringMcpRoutesOptions, type ResolveComposioMcpSessionInput, assertManagedConnectorPreflight, assertMcpPublicPayloadSecretFree, boringMcpAgentSessionNamespace, createBoringMcpAgentBridgeRegistry, createBoringMcpAgentTools, createBoringMcpAppAgentTools, createBoringMcpAppAgentToolsForRequest, createBoringMcpManagedConnectorAdapter, createBoringMcpReadonlyCaller, createBoringMcpServerPlugin, createBoringMcpSourceHandlers, createBoringMcpToolCatalog, createComposioManagedConnectorProvider, createComposioMcpTransport, createHardenedMcpTransport, createLegacyManagedConnectorSourceId, createManagedConnectorAdapter, createManagedConnectorSecretResolver, createManagedConnectorSourceId, createMcpSchemaHash, createMcpSdkStreamableHttpTransport, createMcpSourceStatusPayload, createUserSettingsMcpSourceRegistry, _default as default, evaluateBoringMcpLaunchGate, isActorOwnedMcpSource, listBoringMcpAgentBridgeTools, normalizeMcpCatalogTool, readBoringMcpServerConfig, registerBoringMcpRoutes, requireActorOwnedMcpSource, resolveComposioMcpSession, validateManagedConnectorPreflight, validateMcpSourceId, verifyMcpDisconnectResult };
@@ -883,6 +883,149 @@ function createBoringMcpAgentTools(options) {
883
883
  }));
884
884
  }
885
885
 
886
+ // src/server/appServerBinding.ts
887
+ import { createHash as createHash3 } from "crypto";
888
+ import { HttpError, ERROR_CODES } from "@hachej/boring-core/shared";
889
+ import { withUserSettingsWriteLock } from "@hachej/boring-core/server";
890
+
891
+ // src/server/managedConnectorAdapter.ts
892
+ import { createHash as createHash2 } from "crypto";
893
+ function findConfig(configs, provider) {
894
+ return configs.find((config) => config.provider === provider);
895
+ }
896
+ function createManagedConnectorSourceId(actor, provider) {
897
+ const digest = createHash2("sha256").update(`${actor.workspaceId}\0${actor.userId}\0${provider}`).digest("hex").slice(0, 32);
898
+ return validateMcpSourceId(`managed:${provider}:${digest}`);
899
+ }
900
+ function createLegacyManagedConnectorSourceId(actor, provider) {
901
+ return validateMcpSourceId(`managed:${actor.workspaceId}:${actor.userId}:${provider}`);
902
+ }
903
+ function defaultSourceId(actor, config) {
904
+ return createManagedConnectorSourceId(actor, config.provider);
905
+ }
906
+ function assertSecretFree(value, canaries, code = MCP_ERROR_CODES.SECRET_LEAK_GUARD) {
907
+ if (containsMcpSecretOrCanary(value, canaries)) {
908
+ throw new McpError(code, "Managed connector response contained secret material");
909
+ }
910
+ }
911
+ function safeConnectUrl(rawUrl, config) {
912
+ if (!rawUrl) return void 0;
913
+ let parsed;
914
+ try {
915
+ parsed = new URL(rawUrl);
916
+ } catch {
917
+ throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector returned an invalid connect URL");
918
+ }
919
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password) {
920
+ throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector returned an unsafe connect URL");
921
+ }
922
+ if (config.connectUrlOrigins?.length && !config.connectUrlOrigins.includes(parsed.origin)) {
923
+ throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector returned an unapproved connect URL origin");
924
+ }
925
+ return parsed.toString();
926
+ }
927
+ function createManagedConnectorAdapter(options) {
928
+ const canaries = [...options.preflightEvidence?.redactionCanaries ?? [], ...options.redactionCanaries ?? []];
929
+ const templates = options.templates;
930
+ async function getSecret(provider) {
931
+ const secret = await options.secretResolver.resolveSecret(provider);
932
+ if (secret.storage !== "server-env" && secret.storage !== "server-vault" || !secret.value) {
933
+ throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector secret is not configured");
934
+ }
935
+ return secret;
936
+ }
937
+ function requireConfig(provider) {
938
+ const config = findConfig(options.configs, provider);
939
+ const template = getMcpProviderTemplate(provider, templates);
940
+ if (!config || !template) throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Unknown managed connector provider", { reason: "unsupported_provider" });
941
+ return { config, template };
942
+ }
943
+ return {
944
+ async startConnect(actor, input) {
945
+ const { config } = requireConfig(input.provider);
946
+ const sourceId = validateMcpSourceId((options.sourceIdFactory ?? defaultSourceId)(actor, config));
947
+ const secret = await getSecret(config.provider);
948
+ const secretCanaries = [...canaries, secret.value];
949
+ const response = await options.provider.startConnect({ actor, config, secret, sourceId });
950
+ const connectUrl = safeConnectUrl(response.connectUrl, config);
951
+ assertSecretFree({ ...response, connectUrl }, secretCanaries);
952
+ const source = {
953
+ id: sourceId,
954
+ workspaceId: actor.workspaceId,
955
+ userId: actor.userId,
956
+ provider: config.provider,
957
+ displayName: input.displayName ?? config.displayName,
958
+ status: response.status ?? "unconfigured",
959
+ ownerKind: "user",
960
+ credentialProvider: "composio-managed",
961
+ scopes: config.scopes ? [...config.scopes] : void 0,
962
+ providerAccountLabel: response.providerAccountLabel,
963
+ connectorRef: response.connectorRef
964
+ };
965
+ assertSecretFree(source, secretCanaries);
966
+ const saved = await options.registry.upsertSource(actor, source);
967
+ const result = { ...createMcpSourceStatusPayload(saved), connectUrl };
968
+ assertSecretFree(result, secretCanaries);
969
+ return result;
970
+ },
971
+ async refreshStatus(actor, sourceId) {
972
+ const source = await requireActorOwnedMcpSource(options.registry, actor, sourceId);
973
+ const { config } = requireConfig(source.provider);
974
+ const secret = await getSecret(config.provider);
975
+ const secretCanaries = [...canaries, secret.value];
976
+ const response = await options.provider.refreshStatus({ actor, source, config, secret });
977
+ assertSecretFree(response, secretCanaries);
978
+ const nextSource = {
979
+ ...source,
980
+ status: response.status,
981
+ providerAccountLabel: response.providerAccountLabel ?? source.providerAccountLabel,
982
+ connectorRef: response.connectorRef ?? source.connectorRef,
983
+ lastVerifiedAt: response.lastVerifiedAt
984
+ };
985
+ assertSecretFree(nextSource, secretCanaries);
986
+ const saved = await options.registry.upsertSource(actor, nextSource);
987
+ const result = createMcpSourceStatusPayload(saved);
988
+ assertSecretFree(result, secretCanaries);
989
+ return result;
990
+ },
991
+ async probeSource(actor, sourceId) {
992
+ const source = await requireActorOwnedMcpSource(options.registry, actor, sourceId);
993
+ if (source.status !== "connected") throw new McpError(MCP_ERROR_CODES.SOURCE_UNAVAILABLE, "MCP source is not connected");
994
+ const { config, template } = requireConfig(source.provider);
995
+ const secret = await getSecret(config.provider);
996
+ const response = await options.provider.probe({ actor, source, config, secret });
997
+ assertSecretFree(response, [...canaries, secret.value]);
998
+ const result = {
999
+ sourceId: source.id,
1000
+ provider: source.provider,
1001
+ tools: classifyMcpTools(template, response.tools),
1002
+ resources: response.resources
1003
+ };
1004
+ assertSecretFree(result, [...canaries, secret.value]);
1005
+ return result;
1006
+ },
1007
+ async disconnectSource(actor, sourceId) {
1008
+ if (!options.registry.disconnectSource) throw new McpError(MCP_ERROR_CODES.SOURCE_UNAVAILABLE, "MCP source disconnect is not configured");
1009
+ const source = await requireActorOwnedMcpSource(options.registry, actor, sourceId);
1010
+ let secretCanaries = canaries;
1011
+ if (options.provider.revoke) {
1012
+ try {
1013
+ const { config } = requireConfig(source.provider);
1014
+ const secret = await getSecret(config.provider);
1015
+ await options.provider.revoke({ actor, source, config, secret });
1016
+ secretCanaries = [...canaries, secret.value];
1017
+ } catch (error) {
1018
+ if (!(error instanceof McpError && error.code === MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID)) throw error;
1019
+ }
1020
+ }
1021
+ const disconnected = await options.registry.disconnectSource(actor, source.id);
1022
+ const result = await verifyMcpDisconnectResult(options.registry, actor, source.id, disconnected);
1023
+ assertSecretFree(result, secretCanaries);
1024
+ return result;
1025
+ }
1026
+ };
1027
+ }
1028
+
886
1029
  // src/server/mcpSdkTransport.ts
887
1030
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
888
1031
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -1364,143 +1507,413 @@ function createComposioMcpTransport(options) {
1364
1507
  };
1365
1508
  }
1366
1509
 
1367
- // src/server/managedConnectorAdapter.ts
1368
- import { createHash as createHash2 } from "crypto";
1369
- function findConfig(configs, provider) {
1370
- return configs.find((config) => config.provider === provider);
1510
+ // src/server/appServerBinding.ts
1511
+ var DEFAULT_MAX_READONLY_INPUT_BYTES = 64 * 1024;
1512
+ var MAX_READONLY_INPUT_BYTES = 1024 * 1024;
1513
+ var SOURCE_CONNECT_RATE_LIMIT = { max: 10, timeWindow: "1 minute" };
1514
+ var SOURCE_ACTION_RATE_LIMIT = { max: 60, timeWindow: "1 minute" };
1515
+ var DEFAULT_SECRET_ENV_VARS = ["COMPOSIO_API_KEY"];
1516
+ function resolveSecretEnvVars(config) {
1517
+ return config.secretEnvVars && config.secretEnvVars.length > 0 ? config.secretEnvVars : DEFAULT_SECRET_ENV_VARS;
1518
+ }
1519
+ function resolveComposioSecret(env, secretEnvVars) {
1520
+ for (const name of secretEnvVars) {
1521
+ const value = env[name]?.trim();
1522
+ if (value) return value;
1523
+ }
1524
+ return void 0;
1371
1525
  }
1372
- function createManagedConnectorSourceId(actor, provider) {
1373
- const digest = createHash2("sha256").update(`${actor.workspaceId}\0${actor.userId}\0${provider}`).digest("hex").slice(0, 32);
1374
- return validateMcpSourceId(`managed:${provider}:${digest}`);
1526
+ function parseReadonlyInputLimit(value) {
1527
+ if (!value) return DEFAULT_MAX_READONLY_INPUT_BYTES;
1528
+ const parsed = Number(value);
1529
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > MAX_READONLY_INPUT_BYTES) return DEFAULT_MAX_READONLY_INPUT_BYTES;
1530
+ return parsed;
1375
1531
  }
1376
- function createLegacyManagedConnectorSourceId(actor, provider) {
1377
- return validateMcpSourceId(`managed:${actor.workspaceId}:${actor.userId}:${provider}`);
1532
+ function readBoringMcpServerConfig(env = process.env, options = {}) {
1533
+ const productionEnabled = env.BORING_MCP_PROD_ENABLED === "1";
1534
+ const nonProductionEnabled = env.BORING_MCP_ENABLED !== "0";
1535
+ const secretEnvVars = resolveSecretEnvVars(options);
1536
+ return {
1537
+ enabled: env.NODE_ENV === "production" ? productionEnabled : nonProductionEnabled,
1538
+ composioApiKeyConfigured: Boolean(resolveComposioSecret(env, secretEnvVars)),
1539
+ maxReadonlyInputBytes: parseReadonlyInputLimit(env.BORING_MCP_MAX_READONLY_INPUT_BYTES)
1540
+ };
1378
1541
  }
1379
- function defaultSourceId(actor, config) {
1380
- return createManagedConnectorSourceId(actor, config.provider);
1542
+ function createManagedConnectorSecretResolver(options) {
1543
+ const env = options.env ?? process.env;
1544
+ const secretEnvVars = resolveSecretEnvVars(options);
1545
+ const supportedProviders = new Set(options.configs.map((config) => config.provider));
1546
+ return {
1547
+ async resolveSecret(provider) {
1548
+ if (!supportedProviders.has(provider)) {
1549
+ throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, `Unsupported MCP provider: ${provider}`, { reason: "unsupported_provider" });
1550
+ }
1551
+ const value = resolveComposioSecret(env, secretEnvVars);
1552
+ if (!value) {
1553
+ throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, `${secretEnvVars.join("/")} is not configured`);
1554
+ }
1555
+ return { storage: "server-env", value };
1556
+ }
1557
+ };
1381
1558
  }
1382
- function assertSecretFree(value, canaries, code = MCP_ERROR_CODES.SECRET_LEAK_GUARD) {
1383
- if (containsMcpSecretOrCanary(value, canaries)) {
1384
- throw new McpError(code, "Managed connector response contained secret material");
1559
+ function createBoringMcpManagedConnectorAdapter(options) {
1560
+ return createManagedConnectorAdapter({
1561
+ registry: options.registry,
1562
+ provider: options.provider ?? createComposioManagedConnectorProvider(),
1563
+ secretResolver: createManagedConnectorSecretResolver({
1564
+ env: options.env,
1565
+ configs: options.config.connectorConfigs,
1566
+ secretEnvVars: options.config.secretEnvVars
1567
+ }),
1568
+ templates: DEFAULT_MCP_PROVIDER_TEMPLATES,
1569
+ redactionCanaries: options.config.redactionCanaries,
1570
+ configs: options.config.connectorConfigs
1571
+ });
1572
+ }
1573
+ var USER_SETTINGS_MCP_SOURCES_KEY = "__serverBoringMcpSourcesV1";
1574
+ var WORKSPACE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
1575
+ function requestUserId(request) {
1576
+ const user = request?.user;
1577
+ return typeof user?.id === "string" && user.id.trim() ? user.id : void 0;
1578
+ }
1579
+ function shortHash(value) {
1580
+ return createHash3("sha256").update(value).digest("hex").slice(0, 16);
1581
+ }
1582
+ function safeSessionNamespaceSegment(value) {
1583
+ return value.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80) || "workspace";
1584
+ }
1585
+ function boringMcpAgentSessionNamespace(ctx) {
1586
+ const workspaceSegment = safeSessionNamespaceSegment(ctx.workspaceId);
1587
+ const userId = ctx.userId?.trim() || requestUserId(ctx.request);
1588
+ return `${workspaceSegment}_user_${userId ? shortHash(userId) : "anonymous"}`;
1589
+ }
1590
+ function asRecord(value) {
1591
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1592
+ }
1593
+ function parseString(value) {
1594
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1595
+ }
1596
+ function validateWorkspaceId(value, requestId) {
1597
+ const workspaceId = parseString(value);
1598
+ if (!workspaceId || !WORKSPACE_ID_PATTERN.test(workspaceId)) {
1599
+ throw new HttpError({ status: 400, code: ERROR_CODES.VALIDATION_FAILED, message: "Invalid workspace id", requestId });
1600
+ }
1601
+ return workspaceId;
1602
+ }
1603
+ function routeError(status, code, message, requestId) {
1604
+ return new HttpError({ status, code, message, requestId });
1605
+ }
1606
+ function mcpHttpStatus(error) {
1607
+ switch (error.code) {
1608
+ case MCP_ERROR_CODES.SOURCE_NOT_FOUND:
1609
+ case MCP_ERROR_CODES.TOOL_NOT_FOUND:
1610
+ return { status: 404, code: ERROR_CODES.NOT_FOUND };
1611
+ case MCP_ERROR_CODES.SOURCE_FORBIDDEN:
1612
+ return { status: 403, code: ERROR_CODES.FORBIDDEN };
1613
+ case MCP_ERROR_CODES.INPUT_INVALID:
1614
+ case MCP_ERROR_CODES.RESOURCE_URI_INVALID:
1615
+ case MCP_ERROR_CODES.PROVIDER_TOOL_DRIFT:
1616
+ case MCP_ERROR_CODES.TOOL_NOT_ALLOWED:
1617
+ return { status: 400, code: ERROR_CODES.VALIDATION_FAILED };
1618
+ case MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID:
1619
+ return asRecord(error.details).reason === "unsupported_provider" ? { status: 400, code: ERROR_CODES.VALIDATION_FAILED } : { status: 503, code: ERROR_CODES.CONFIG_VALIDATION_FAILED };
1620
+ case MCP_ERROR_CODES.SOURCE_UNAVAILABLE:
1621
+ return { status: 409, code: ERROR_CODES.VALIDATION_FAILED };
1622
+ case MCP_ERROR_CODES.RESOURCE_LIMIT_EXCEEDED:
1623
+ return { status: 413, code: ERROR_CODES.VALIDATION_FAILED };
1624
+ case MCP_ERROR_CODES.PROVIDER_TIMEOUT:
1625
+ return { status: 504, code: ERROR_CODES.INTERNAL_ERROR };
1626
+ case MCP_ERROR_CODES.PROVIDER_ERROR:
1627
+ return { status: 502, code: ERROR_CODES.INTERNAL_ERROR };
1628
+ case MCP_ERROR_CODES.SECRET_LEAK_GUARD:
1629
+ return { status: 500, code: ERROR_CODES.INTERNAL_ERROR };
1630
+ default:
1631
+ return { status: 500, code: ERROR_CODES.INTERNAL_ERROR };
1632
+ }
1633
+ }
1634
+ async function withMcpHttpErrors(requestId, fn) {
1635
+ try {
1636
+ return await fn();
1637
+ } catch (error) {
1638
+ if (error instanceof McpError) {
1639
+ const mapped = mcpHttpStatus(error);
1640
+ throw routeError(mapped.status, mapped.code, error.message, requestId);
1641
+ }
1642
+ throw error;
1385
1643
  }
1386
1644
  }
1387
- function safeConnectUrl(rawUrl, config) {
1388
- if (!rawUrl) return void 0;
1389
- let parsed;
1645
+ function sourceFromUnknown(value) {
1646
+ const record2 = asRecord(value);
1647
+ const id = parseString(record2.id);
1648
+ const workspaceId = parseString(record2.workspaceId);
1649
+ const userId = parseString(record2.userId);
1650
+ const provider = parseString(record2.provider);
1651
+ const displayName2 = parseString(record2.displayName);
1652
+ const status = parseString(record2.status);
1653
+ const ownerKind = parseString(record2.ownerKind);
1654
+ const credentialProvider = parseString(record2.credentialProvider);
1655
+ if (!id || !workspaceId || !userId || !provider || !displayName2 || !status || !ownerKind || !credentialProvider) return void 0;
1656
+ if (!["connected", "expired", "revoked", "error", "unconfigured"].includes(status)) return void 0;
1657
+ return {
1658
+ id,
1659
+ workspaceId,
1660
+ userId,
1661
+ provider,
1662
+ displayName: displayName2,
1663
+ status,
1664
+ ownerKind,
1665
+ credentialProvider,
1666
+ scopes: Array.isArray(record2.scopes) ? record2.scopes.filter((item) => typeof item === "string") : void 0,
1667
+ providerAccountLabel: parseString(record2.providerAccountLabel),
1668
+ connectorRef: asRecord(record2.connectorRef),
1669
+ lastVerifiedAt: parseString(record2.lastVerifiedAt),
1670
+ createdAt: parseString(record2.createdAt),
1671
+ updatedAt: parseString(record2.updatedAt)
1672
+ };
1673
+ }
1674
+ function readRawStoredSources(settings, actor) {
1675
+ const root = asRecord(settings[USER_SETTINGS_MCP_SOURCES_KEY]);
1676
+ const workspaceSources = asRecord(root[actor.workspaceId]);
1677
+ return Object.values(workspaceSources).map(sourceFromUnknown).filter((source) => Boolean(source && source.workspaceId === actor.workspaceId && source.userId === actor.userId));
1678
+ }
1679
+ function normalizeStoredSourceId(actor, source) {
1390
1680
  try {
1391
- parsed = new URL(rawUrl);
1681
+ const legacyId = createLegacyManagedConnectorSourceId(actor, source.provider);
1682
+ return source.id === legacyId ? { ...source, id: createManagedConnectorSourceId(actor, source.provider) } : source;
1392
1683
  } catch {
1393
- throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector returned an invalid connect URL");
1394
- }
1395
- if (parsed.protocol !== "https:" || parsed.username || parsed.password) {
1396
- throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector returned an unsafe connect URL");
1397
- }
1398
- if (config.connectUrlOrigins?.length && !config.connectUrlOrigins.includes(parsed.origin)) {
1399
- throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector returned an unapproved connect URL origin");
1684
+ return source;
1400
1685
  }
1401
- return parsed.toString();
1402
1686
  }
1403
- function createManagedConnectorAdapter(options) {
1404
- const canaries = [...options.preflightEvidence?.redactionCanaries ?? [], ...options.redactionCanaries ?? []];
1405
- const templates = options.templates;
1406
- async function getSecret(provider) {
1407
- const secret = await options.secretResolver.resolveSecret(provider);
1408
- if (secret.storage !== "server-env" && secret.storage !== "server-vault" || !secret.value) {
1409
- throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Managed connector secret is not configured");
1410
- }
1411
- return secret;
1687
+ function sourceStatusRank(source) {
1688
+ switch (source.status) {
1689
+ case "connected":
1690
+ return 5;
1691
+ case "expired":
1692
+ case "error":
1693
+ return 4;
1694
+ case "unconfigured":
1695
+ return 3;
1696
+ case "revoked":
1697
+ return 1;
1698
+ default:
1699
+ return 0;
1412
1700
  }
1413
- function requireConfig(provider) {
1414
- const config = findConfig(options.configs, provider);
1415
- const template = getMcpProviderTemplate(provider, templates);
1416
- if (!config || !template) throw new McpError(MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID, "Unknown managed connector provider", { reason: "unsupported_provider" });
1417
- return { config, template };
1701
+ }
1702
+ function dedupeStoredSources(sources) {
1703
+ const byId = /* @__PURE__ */ new Map();
1704
+ for (const source of sources) {
1705
+ const current = byId.get(source.id);
1706
+ if (!current || sourceStatusRank(source) >= sourceStatusRank(current)) byId.set(source.id, source);
1418
1707
  }
1708
+ return [...byId.values()];
1709
+ }
1710
+ function readStoredSources(settings, actor) {
1711
+ return dedupeStoredSources(readRawStoredSources(settings, actor).map((source) => normalizeStoredSourceId(actor, source)));
1712
+ }
1713
+ function existingLegacySourceIdFor(actor, source, rawSources) {
1714
+ try {
1715
+ const legacyId = createLegacyManagedConnectorSourceId(actor, source.provider);
1716
+ return legacyId !== source.id && rawSources.some((item) => item.id === legacyId) ? legacyId : void 0;
1717
+ } catch {
1718
+ return void 0;
1719
+ }
1720
+ }
1721
+ async function saveStoredSource(app, actor, source, removeSourceIds = []) {
1722
+ const patch = app.userStore.patchUserSettingsJsonPath;
1723
+ if (patch && removeSourceIds.length === 0) {
1724
+ const updated = await patch.call(
1725
+ app.userStore,
1726
+ actor.userId,
1727
+ app.config.appId,
1728
+ [USER_SETTINGS_MCP_SOURCES_KEY, actor.workspaceId, source.id],
1729
+ source
1730
+ );
1731
+ return readStoredSources(updated.settings, actor);
1732
+ }
1733
+ return await withUserSettingsWriteLock(actor.userId, app.config.appId, async () => {
1734
+ const current = await app.userStore.getUserSettings(actor.userId, app.config.appId);
1735
+ const root = asRecord(current.settings[USER_SETTINGS_MCP_SOURCES_KEY]);
1736
+ const removeIds = /* @__PURE__ */ new Set([source.id, ...removeSourceIds]);
1737
+ const sources = readRawStoredSources(current.settings, actor);
1738
+ const nextSources = [...sources.filter((item) => !removeIds.has(item.id)), source];
1739
+ const nextSettings = {
1740
+ ...current.settings,
1741
+ [USER_SETTINGS_MCP_SOURCES_KEY]: {
1742
+ ...root,
1743
+ [actor.workspaceId]: Object.fromEntries(nextSources.map((item) => [item.id, item]))
1744
+ }
1745
+ };
1746
+ await app.userStore.putUserSettings(actor.userId, app.config.appId, { settings: nextSettings });
1747
+ return readStoredSources(nextSettings, actor);
1748
+ });
1749
+ }
1750
+ function createUserSettingsMcpSourceRegistry(app, actorScope) {
1419
1751
  return {
1420
- async startConnect(actor, input) {
1421
- const { config } = requireConfig(input.provider);
1422
- const sourceId = validateMcpSourceId((options.sourceIdFactory ?? defaultSourceId)(actor, config));
1423
- const secret = await getSecret(config.provider);
1424
- const secretCanaries = [...canaries, secret.value];
1425
- const response = await options.provider.startConnect({ actor, config, secret, sourceId });
1426
- const connectUrl = safeConnectUrl(response.connectUrl, config);
1427
- assertSecretFree({ ...response, connectUrl }, secretCanaries);
1428
- const source = {
1429
- id: sourceId,
1430
- workspaceId: actor.workspaceId,
1431
- userId: actor.userId,
1432
- provider: config.provider,
1433
- displayName: input.displayName ?? config.displayName,
1434
- status: response.status ?? "unconfigured",
1435
- ownerKind: "user",
1436
- credentialProvider: "composio-managed",
1437
- scopes: config.scopes ? [...config.scopes] : void 0,
1438
- providerAccountLabel: response.providerAccountLabel,
1439
- connectorRef: response.connectorRef
1440
- };
1441
- assertSecretFree(source, secretCanaries);
1442
- const saved = await options.registry.upsertSource(actor, source);
1443
- const result = { ...createMcpSourceStatusPayload(saved), connectUrl };
1444
- assertSecretFree(result, secretCanaries);
1445
- return result;
1752
+ async listSources(actor) {
1753
+ const current = await app.userStore.getUserSettings(actor.userId, app.config.appId);
1754
+ return readStoredSources(current.settings, actor);
1446
1755
  },
1447
- async refreshStatus(actor, sourceId) {
1448
- const source = await requireActorOwnedMcpSource(options.registry, actor, sourceId);
1449
- const { config } = requireConfig(source.provider);
1450
- const secret = await getSecret(config.provider);
1451
- const secretCanaries = [...canaries, secret.value];
1452
- const response = await options.provider.refreshStatus({ actor, source, config, secret });
1453
- assertSecretFree(response, secretCanaries);
1454
- const nextSource = {
1455
- ...source,
1456
- status: response.status,
1457
- providerAccountLabel: response.providerAccountLabel ?? source.providerAccountLabel,
1458
- connectorRef: response.connectorRef ?? source.connectorRef,
1459
- lastVerifiedAt: response.lastVerifiedAt
1460
- };
1461
- assertSecretFree(nextSource, secretCanaries);
1462
- const saved = await options.registry.upsertSource(actor, nextSource);
1463
- const result = createMcpSourceStatusPayload(saved);
1464
- assertSecretFree(result, secretCanaries);
1465
- return result;
1756
+ async getSource(sourceId) {
1757
+ const current = await app.userStore.getUserSettings(actorScope.userId, app.config.appId);
1758
+ const source = readStoredSources(current.settings, actorScope).find((item) => item.id === sourceId);
1759
+ if (source) return source;
1760
+ const legacySource = readRawStoredSources(current.settings, actorScope).find((item) => item.id === sourceId);
1761
+ return legacySource ? normalizeStoredSourceId(actorScope, legacySource) : void 0;
1466
1762
  },
1467
- async probeSource(actor, sourceId) {
1468
- const source = await requireActorOwnedMcpSource(options.registry, actor, sourceId);
1469
- if (source.status !== "connected") throw new McpError(MCP_ERROR_CODES.SOURCE_UNAVAILABLE, "MCP source is not connected");
1470
- const { config, template } = requireConfig(source.provider);
1471
- const secret = await getSecret(config.provider);
1472
- const response = await options.provider.probe({ actor, source, config, secret });
1473
- assertSecretFree(response, [...canaries, secret.value]);
1474
- const result = {
1475
- sourceId: source.id,
1476
- provider: source.provider,
1477
- tools: classifyMcpTools(template, response.tools),
1478
- resources: response.resources
1479
- };
1480
- assertSecretFree(result, [...canaries, secret.value]);
1481
- return result;
1763
+ async upsertSource(actor, source) {
1764
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1765
+ const currentSettings = await app.userStore.getUserSettings(actor.userId, app.config.appId);
1766
+ const current = readStoredSources(currentSettings.settings, actor);
1767
+ const rawSources = readRawStoredSources(currentSettings.settings, actor);
1768
+ const existing = current.find((item) => item.id === source.id);
1769
+ const next = { ...source, updatedAt: now, createdAt: source.createdAt ?? existing?.createdAt ?? now };
1770
+ const legacyId = existingLegacySourceIdFor(actor, next, rawSources);
1771
+ await saveStoredSource(app, actor, next, legacyId ? [legacyId] : []);
1772
+ return next;
1482
1773
  },
1483
1774
  async disconnectSource(actor, sourceId) {
1484
- if (!options.registry.disconnectSource) throw new McpError(MCP_ERROR_CODES.SOURCE_UNAVAILABLE, "MCP source disconnect is not configured");
1485
- const source = await requireActorOwnedMcpSource(options.registry, actor, sourceId);
1486
- let secretCanaries = canaries;
1487
- if (options.provider.revoke) {
1488
- try {
1489
- const { config } = requireConfig(source.provider);
1490
- const secret = await getSecret(config.provider);
1491
- await options.provider.revoke({ actor, source, config, secret });
1492
- secretCanaries = [...canaries, secret.value];
1493
- } catch (error) {
1494
- if (!(error instanceof McpError && error.code === MCP_ERROR_CODES.PROVIDER_CONFIG_INVALID)) throw error;
1495
- }
1496
- }
1497
- const disconnected = await options.registry.disconnectSource(actor, source.id);
1498
- const result = await verifyMcpDisconnectResult(options.registry, actor, source.id, disconnected);
1499
- assertSecretFree(result, secretCanaries);
1500
- return result;
1775
+ const currentSettings = await app.userStore.getUserSettings(actor.userId, app.config.appId);
1776
+ const source = readStoredSources(currentSettings.settings, actor).find((item) => item.id === sourceId) ?? (() => {
1777
+ const legacySource = readRawStoredSources(currentSettings.settings, actor).find((item) => item.id === sourceId);
1778
+ return legacySource ? normalizeStoredSourceId(actor, legacySource) : void 0;
1779
+ })();
1780
+ if (!source) return void 0;
1781
+ const disconnected = { ...source, status: "revoked", updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1782
+ const legacyId = existingLegacySourceIdFor(actor, disconnected, readRawStoredSources(currentSettings.settings, actor));
1783
+ await saveStoredSource(app, actor, disconnected, legacyId ? [legacyId] : []);
1784
+ return disconnected;
1501
1785
  }
1502
1786
  };
1503
1787
  }
1788
+ function createComposioMcpTransportFor(config, env) {
1789
+ return createComposioMcpTransport({
1790
+ secretResolver: createManagedConnectorSecretResolver({ env, configs: config.connectorConfigs, secretEnvVars: config.secretEnvVars }),
1791
+ configs: config.connectorConfigs,
1792
+ clientName: config.clientName ?? "boring-mcp",
1793
+ clientVersion: config.clientVersion ?? "0.0.0"
1794
+ });
1795
+ }
1796
+ function createBoringMcpAppAgentTools(app, actor, options) {
1797
+ const runtimeConfig = readBoringMcpServerConfig(options.env, { secretEnvVars: options.config.secretEnvVars });
1798
+ if (!runtimeConfig.enabled) return [];
1799
+ const registry = createUserSettingsMcpSourceRegistry(app, actor);
1800
+ const transport = options.transport ?? createComposioMcpTransportFor(options.config, options.env);
1801
+ return createBoringMcpAgentTools({
1802
+ registry,
1803
+ transport,
1804
+ resolveActor: () => actor,
1805
+ templates: DEFAULT_MCP_PROVIDER_TEMPLATES,
1806
+ hardening: { gate: new InMemoryMcpRateBudgetGate({ maxCalls: 100, maxToolCalls: 10, windowMs: 6e4 }), timeoutMs: 3e4 },
1807
+ maxReadonlyInputBytes: runtimeConfig.maxReadonlyInputBytes
1808
+ });
1809
+ }
1810
+ function createBoringMcpAppAgentToolsForRequest(app, ctx, options) {
1811
+ const userId = typeof ctx.authSubject === "string" && ctx.authSubject.trim() ? ctx.authSubject.trim() : void 0;
1812
+ if (!userId) return [];
1813
+ return createBoringMcpAppAgentTools(app, { workspaceId: ctx.workspaceId, userId }, options);
1814
+ }
1815
+ async function requireBoringMcpActor(app, request) {
1816
+ const body = asRecord(request.body);
1817
+ const query = asRecord(request.query);
1818
+ const workspaceId = validateWorkspaceId(
1819
+ request.headers["x-boring-workspace-id"] ?? body.workspaceId ?? query.workspaceId,
1820
+ request.id
1821
+ );
1822
+ const user = request.user;
1823
+ if (!user) throw routeError(401, ERROR_CODES.UNAUTHORIZED, "Authentication required", request.id);
1824
+ const workspace = await app.workspaceStore.get(workspaceId);
1825
+ if (!workspace || workspace.appId !== app.config.appId) throw routeError(404, ERROR_CODES.NOT_FOUND, "Workspace not found", request.id);
1826
+ const role = await app.workspaceStore.getMemberRole(workspaceId, user.id);
1827
+ if (!role) throw routeError(403, ERROR_CODES.NOT_MEMBER, "Not a member of this workspace", request.id);
1828
+ return { workspaceId, userId: user.id };
1829
+ }
1830
+ function sourceActionRateLimitKey(request) {
1831
+ const workspaceId = typeof request.headers["x-boring-workspace-id"] === "string" ? request.headers["x-boring-workspace-id"] : "unknown-workspace";
1832
+ return `${request.user?.id ?? request.ip}:${workspaceId}`;
1833
+ }
1834
+ function sourceIdFromBody(body, requestId) {
1835
+ const sourceId = parseString(asRecord(body).sourceId);
1836
+ if (!sourceId) throw routeError(400, ERROR_CODES.VALIDATION_FAILED, "sourceId is required", requestId);
1837
+ return sourceId;
1838
+ }
1839
+ function providerFromBody(body, requestId) {
1840
+ const provider = parseString(asRecord(body).provider);
1841
+ if (!provider) throw routeError(400, ERROR_CODES.VALIDATION_FAILED, "provider is required", requestId);
1842
+ return provider;
1843
+ }
1844
+ function registerBoringMcpRoutes(app, options) {
1845
+ const whenDisabled = options.whenDisabled ?? "skip";
1846
+ const enabled = readBoringMcpServerConfig(options.env, { secretEnvVars: options.config.secretEnvVars }).enabled;
1847
+ if (!enabled && whenDisabled === "skip") return;
1848
+ const assertEnabled = (requestId) => {
1849
+ if (!enabled) throw routeError(503, ERROR_CODES.INTERNAL_ERROR, "MCP source routes are disabled for this deployment", requestId);
1850
+ };
1851
+ const routeTransport = options.transport ?? createComposioMcpTransportFor(options.config, options.env);
1852
+ const routeGate = new InMemoryMcpRateBudgetGate({ maxCalls: 100, maxToolCalls: 10, windowMs: 6e4 });
1853
+ function adapterFor(actor) {
1854
+ const registry = createUserSettingsMcpSourceRegistry(app, actor);
1855
+ return {
1856
+ registry,
1857
+ adapter: createBoringMcpManagedConnectorAdapter({ config: options.config, registry, provider: options.provider, env: options.env })
1858
+ };
1859
+ }
1860
+ function handlersFor(actor) {
1861
+ return createBoringMcpSourceHandlers({
1862
+ registry: createUserSettingsMcpSourceRegistry(app, actor),
1863
+ transport: routeTransport,
1864
+ templates: DEFAULT_MCP_PROVIDER_TEMPLATES,
1865
+ hardening: { gate: routeGate, timeoutMs: 3e4 }
1866
+ });
1867
+ }
1868
+ app.get("/api/v1/boring-mcp/sources", async (request) => {
1869
+ assertEnabled(request.id);
1870
+ const actor = await requireBoringMcpActor(app, request);
1871
+ const { registry } = adapterFor(actor);
1872
+ const sources = await registry.listSources(actor);
1873
+ return { sourceStatuses: sources.map(createMcpSourceStatusPayload) };
1874
+ });
1875
+ app.post("/api/v1/boring-mcp/connect", {
1876
+ config: { rateLimit: { ...SOURCE_CONNECT_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1877
+ }, async (request, reply) => {
1878
+ assertEnabled(request.id);
1879
+ const actor = await requireBoringMcpActor(app, request);
1880
+ const { adapter } = adapterFor(actor);
1881
+ const provider = providerFromBody(request.body, request.id);
1882
+ const result = await withMcpHttpErrors(request.id, () => adapter.startConnect(actor, { provider }));
1883
+ const { connectUrl, ...status } = result;
1884
+ reply.status(201);
1885
+ return { status, connectUrl };
1886
+ });
1887
+ app.post("/api/v1/boring-mcp/refresh", {
1888
+ config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1889
+ }, async (request) => {
1890
+ assertEnabled(request.id);
1891
+ const actor = await requireBoringMcpActor(app, request);
1892
+ const { adapter } = adapterFor(actor);
1893
+ const result = await withMcpHttpErrors(request.id, () => adapter.refreshStatus(actor, sourceIdFromBody(request.body, request.id)));
1894
+ return { status: result };
1895
+ });
1896
+ app.post("/api/v1/boring-mcp/disconnect", {
1897
+ config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1898
+ }, async (request) => {
1899
+ assertEnabled(request.id);
1900
+ const actor = await requireBoringMcpActor(app, request);
1901
+ const status = await withMcpHttpErrors(request.id, () => adapterFor(actor).adapter.disconnectSource(actor, sourceIdFromBody(request.body, request.id)));
1902
+ return { status };
1903
+ });
1904
+ app.post("/api/v1/boring-mcp/tools", {
1905
+ config: { rateLimit: { ...SOURCE_ACTION_RATE_LIMIT, keyGenerator: sourceActionRateLimitKey } }
1906
+ }, async (request) => {
1907
+ assertEnabled(request.id);
1908
+ const actor = await requireBoringMcpActor(app, request);
1909
+ const body = asRecord(request.body);
1910
+ const result = await withMcpHttpErrors(request.id, () => handlersFor(actor).searchTools(actor, {
1911
+ sourceId: sourceIdFromBody(request.body, request.id),
1912
+ refresh: body.refresh === true
1913
+ }));
1914
+ return { tools: result.tools };
1915
+ });
1916
+ }
1504
1917
 
1505
1918
  // src/server/managedConnectorPreflight.ts
1506
1919
  var VENDOR_RISK_FIELDS = [
@@ -1585,6 +1998,7 @@ export {
1585
1998
  assertManagedConnectorPreflight,
1586
1999
  assertMcpPublicPayloadSecretFree,
1587
2000
  assertMcpToolAllowed,
2001
+ boringMcpAgentSessionNamespace,
1588
2002
  classifyMcpTool,
1589
2003
  classifyMcpTools,
1590
2004
  containsMcpCanary,
@@ -1592,6 +2006,9 @@ export {
1592
2006
  containsMcpSecretOrCanary,
1593
2007
  createBoringMcpAgentBridgeRegistry,
1594
2008
  createBoringMcpAgentTools,
2009
+ createBoringMcpAppAgentTools,
2010
+ createBoringMcpAppAgentToolsForRequest,
2011
+ createBoringMcpManagedConnectorAdapter,
1595
2012
  createBoringMcpReadonlyCaller,
1596
2013
  createBoringMcpServerPlugin,
1597
2014
  createBoringMcpSourceHandlers,
@@ -1601,10 +2018,12 @@ export {
1601
2018
  createHardenedMcpTransport,
1602
2019
  createLegacyManagedConnectorSourceId,
1603
2020
  createManagedConnectorAdapter,
2021
+ createManagedConnectorSecretResolver,
1604
2022
  createManagedConnectorSourceId,
1605
2023
  createMcpSchemaHash,
1606
2024
  createMcpSdkStreamableHttpTransport,
1607
2025
  createMcpSourceStatusPayload,
2026
+ createUserSettingsMcpSourceRegistry,
1608
2027
  server_default as default,
1609
2028
  doctorMcpSource,
1610
2029
  evaluateBoringMcpLaunchGate,
@@ -1612,7 +2031,9 @@ export {
1612
2031
  isActorOwnedMcpSource,
1613
2032
  listBoringMcpAgentBridgeTools,
1614
2033
  normalizeMcpCatalogTool,
2034
+ readBoringMcpServerConfig,
1615
2035
  redactMcpSecrets,
2036
+ registerBoringMcpRoutes,
1616
2037
  requireActorOwnedMcpSource,
1617
2038
  resolveComposioMcpSession,
1618
2039
  toMcpSourceDto,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-mcp",
3
- "version": "0.1.78",
3
+ "version": "0.1.79",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -46,11 +46,14 @@
46
46
  },
47
47
  "sideEffects": false,
48
48
  "peerDependencies": {
49
+ "fastify": "^5.9.0",
49
50
  "react": "^18.0.0 || ^19.0.0",
50
51
  "react-dom": "^18.0.0 || ^19.0.0",
51
- "@hachej/boring-workspace": "0.1.78"
52
+ "@hachej/boring-core": "0.1.79",
53
+ "@hachej/boring-workspace": "0.1.79"
52
54
  },
53
55
  "devDependencies": {
56
+ "fastify": "^5.9.0",
54
57
  "@testing-library/jest-dom": "^6.9.1",
55
58
  "@testing-library/react": "^16.3.0",
56
59
  "@types/node": "^22.20.0",
@@ -63,7 +66,8 @@
63
66
  "tsup": "^8.4.0",
64
67
  "typescript": "~6.0.3",
65
68
  "vitest": "^4.1.9",
66
- "@hachej/boring-workspace": "0.1.78"
69
+ "@hachej/boring-core": "0.1.79",
70
+ "@hachej/boring-workspace": "0.1.79"
67
71
  },
68
72
  "dependencies": {
69
73
  "@modelcontextprotocol/sdk": "^1.29.0"