@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.
- package/dist/server/index.d.ts +74 -1
- package/dist/server/index.js +538 -117
- package/package.json +7 -3
package/dist/server/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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/
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
|
1377
|
-
|
|
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
|
|
1380
|
-
|
|
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
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
1681
|
+
const legacyId = createLegacyManagedConnectorSourceId(actor, source.provider);
|
|
1682
|
+
return source.id === legacyId ? { ...source, id: createManagedConnectorSourceId(actor, source.provider) } : source;
|
|
1392
1683
|
} catch {
|
|
1393
|
-
|
|
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
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
|
1421
|
-
const
|
|
1422
|
-
|
|
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
|
|
1448
|
-
const
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
const
|
|
1452
|
-
|
|
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
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1485
|
-
const source =
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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"
|