mcp4openapi 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -63
- package/dist/scripts/validate-profile.js +3 -3
- package/dist/scripts/validate-profile.js.map +1 -1
- package/dist/src/{oauth-provider.d.ts → auth/oauth-provider.d.ts} +7 -2
- package/dist/src/auth/oauth-provider.d.ts.map +1 -0
- package/dist/src/{oauth-provider.js → auth/oauth-provider.js} +30 -2
- package/dist/src/auth/oauth-provider.js.map +1 -0
- package/dist/src/core/cli-config.d.ts +9 -0
- package/dist/src/core/cli-config.d.ts.map +1 -0
- package/dist/src/core/cli-config.js +124 -0
- package/dist/src/core/cli-config.js.map +1 -0
- package/dist/src/{constants.d.ts → core/constants.d.ts} +1 -0
- package/dist/src/core/constants.d.ts.map +1 -0
- package/dist/src/{constants.js → core/constants.js} +1 -0
- package/dist/src/core/constants.js.map +1 -0
- package/dist/src/{errors.d.ts → core/errors.d.ts} +6 -0
- package/dist/src/core/errors.d.ts.map +1 -0
- package/dist/src/{errors.js → core/errors.js} +15 -6
- package/dist/src/core/errors.js.map +1 -0
- package/dist/src/core/filtering.d.ts +19 -0
- package/dist/src/core/filtering.d.ts.map +1 -0
- package/dist/src/core/filtering.js +292 -0
- package/dist/src/core/filtering.js.map +1 -0
- package/dist/src/core/index.d.ts +26 -0
- package/dist/src/core/index.d.ts.map +1 -0
- package/dist/src/core/index.js +275 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/lib.d.ts +8 -0
- package/dist/src/core/lib.d.ts.map +1 -0
- package/dist/src/core/lib.js +7 -0
- package/dist/src/core/lib.js.map +1 -0
- package/dist/src/{logger.d.ts → core/logger.d.ts} +6 -1
- package/dist/src/core/logger.d.ts.map +1 -0
- package/dist/src/{logger.js → core/logger.js} +30 -2
- package/dist/src/core/logger.js.map +1 -0
- package/dist/src/{metrics.d.ts → core/metrics.d.ts} +11 -0
- package/dist/src/core/metrics.d.ts.map +1 -0
- package/dist/src/{metrics.js → core/metrics.js} +61 -0
- package/dist/src/core/metrics.js.map +1 -0
- package/dist/src/core/naming-warnings.d.ts.map +1 -0
- package/dist/src/core/naming-warnings.js.map +1 -0
- package/dist/src/core/naming.d.ts.map +1 -0
- package/dist/src/core/naming.js.map +1 -0
- package/dist/src/generated-schemas.d.ts +245 -79
- package/dist/src/generated-schemas.d.ts.map +1 -1
- package/dist/src/generated-schemas.js +14 -2
- package/dist/src/generated-schemas.js.map +1 -1
- package/dist/src/index.d.ts +1 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -170
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib.d.ts +1 -7
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +1 -6
- package/dist/src/lib.js.map +1 -1
- package/dist/src/mcp/mcp-server-manager.d.ts +20 -0
- package/dist/src/mcp/mcp-server-manager.d.ts.map +1 -0
- package/dist/src/mcp/mcp-server-manager.js +38 -0
- package/dist/src/mcp/mcp-server-manager.js.map +1 -0
- package/dist/src/{mcp-server.d.ts → mcp/mcp-server.d.ts} +31 -1
- package/dist/src/mcp/mcp-server.d.ts.map +1 -0
- package/dist/src/{mcp-server.js → mcp/mcp-server.js} +547 -146
- package/dist/src/mcp/mcp-server.js.map +1 -0
- package/dist/src/{openapi-parser.d.ts → openapi/openapi-parser.d.ts} +1 -1
- package/dist/src/openapi/openapi-parser.d.ts.map +1 -0
- package/dist/src/{openapi-parser.js → openapi/openapi-parser.js} +2 -2
- package/dist/src/openapi/openapi-parser.js.map +1 -0
- package/dist/src/{profile-loader.d.ts → profile/profile-loader.d.ts} +3 -2
- package/dist/src/profile/profile-loader.d.ts.map +1 -0
- package/dist/src/{profile-loader.js → profile/profile-loader.js} +17 -6
- package/dist/src/profile/profile-loader.js.map +1 -0
- package/dist/src/profile/profile-registry.d.ts +18 -0
- package/dist/src/profile/profile-registry.d.ts.map +1 -0
- package/dist/src/profile/profile-registry.js +26 -0
- package/dist/src/profile/profile-registry.js.map +1 -0
- package/dist/src/profile/profile-resolver.d.ts +25 -0
- package/dist/src/profile/profile-resolver.d.ts.map +1 -0
- package/dist/src/profile/profile-resolver.js +204 -0
- package/dist/src/profile/profile-resolver.js.map +1 -0
- package/dist/src/profile/startup-profile.d.ts +17 -0
- package/dist/src/profile/startup-profile.d.ts.map +1 -0
- package/dist/src/profile/startup-profile.js +30 -0
- package/dist/src/profile/startup-profile.js.map +1 -0
- package/dist/src/profile/startup-validation.d.ts +11 -0
- package/dist/src/profile/startup-validation.d.ts.map +1 -0
- package/dist/src/profile/startup-validation.js +21 -0
- package/dist/src/profile/startup-validation.js.map +1 -0
- package/dist/src/testing/dynamic-mock-server.d.ts +24 -0
- package/dist/src/testing/dynamic-mock-server.d.ts.map +1 -0
- package/dist/src/testing/dynamic-mock-server.js +138 -0
- package/dist/src/testing/dynamic-mock-server.js.map +1 -0
- package/dist/src/testing/request-assertions.d.ts +5 -0
- package/dist/src/testing/request-assertions.d.ts.map +1 -0
- package/dist/src/testing/request-assertions.js +165 -0
- package/dist/src/testing/request-assertions.js.map +1 -0
- package/dist/src/testing/template-utils.d.ts +10 -0
- package/dist/src/testing/template-utils.d.ts.map +1 -0
- package/dist/src/testing/template-utils.js +72 -0
- package/dist/src/testing/template-utils.js.map +1 -0
- package/dist/src/testing/test-http-utils.d.ts +1 -1
- package/dist/src/testing/test-http-utils.d.ts.map +1 -1
- package/dist/src/testing/test-http-utils.js +1 -1
- package/dist/src/testing/test-http-utils.js.map +1 -1
- package/dist/src/testing/test-loader.d.ts +6 -0
- package/dist/src/testing/test-loader.d.ts.map +1 -0
- package/dist/src/testing/test-loader.js +212 -0
- package/dist/src/testing/test-loader.js.map +1 -0
- package/dist/src/testing/test-schema.d.ts +1270 -0
- package/dist/src/testing/test-schema.d.ts.map +1 -0
- package/dist/src/testing/test-schema.js +76 -0
- package/dist/src/testing/test-schema.js.map +1 -0
- package/dist/src/tool-filter/compat.d.ts +49 -0
- package/dist/src/tool-filter/compat.d.ts.map +1 -0
- package/dist/src/tool-filter/compat.js +72 -0
- package/dist/src/tool-filter/compat.js.map +1 -0
- package/dist/src/tool-filter/config/env-config-parser.d.ts +38 -0
- package/dist/src/tool-filter/config/env-config-parser.d.ts.map +1 -0
- package/dist/src/tool-filter/config/env-config-parser.js +103 -0
- package/dist/src/tool-filter/config/env-config-parser.js.map +1 -0
- package/dist/src/tool-filter/config/header-config-parser.d.ts +37 -0
- package/dist/src/tool-filter/config/header-config-parser.d.ts.map +1 -0
- package/dist/src/tool-filter/config/header-config-parser.js +118 -0
- package/dist/src/tool-filter/config/header-config-parser.js.map +1 -0
- package/dist/src/tool-filter/errors.d.ts +18 -0
- package/dist/src/tool-filter/errors.d.ts.map +1 -0
- package/dist/src/tool-filter/errors.js +21 -0
- package/dist/src/tool-filter/errors.js.map +1 -0
- package/dist/src/tool-filter/filter/filter-engine.d.ts +45 -0
- package/dist/src/tool-filter/filter/filter-engine.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/filter-engine.js +94 -0
- package/dist/src/tool-filter/filter/filter-engine.js.map +1 -0
- package/dist/src/tool-filter/filter/filter-rules.d.ts +44 -0
- package/dist/src/tool-filter/filter/filter-rules.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/filter-rules.js +72 -0
- package/dist/src/tool-filter/filter/filter-rules.js.map +1 -0
- package/dist/src/tool-filter/filter/global-tool-filter.d.ts +40 -0
- package/dist/src/tool-filter/filter/global-tool-filter.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/global-tool-filter.js +92 -0
- package/dist/src/tool-filter/filter/global-tool-filter.js.map +1 -0
- package/dist/src/tool-filter/filter/session-tool-filter.d.ts +29 -0
- package/dist/src/tool-filter/filter/session-tool-filter.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/session-tool-filter.js +69 -0
- package/dist/src/tool-filter/filter/session-tool-filter.js.map +1 -0
- package/dist/src/tool-filter/index.d.ts +25 -0
- package/dist/src/tool-filter/index.d.ts.map +1 -0
- package/dist/src/tool-filter/index.js +30 -0
- package/dist/src/tool-filter/index.js.map +1 -0
- package/dist/src/tool-filter/integration/tool-filter-service.d.ts +44 -0
- package/dist/src/tool-filter/integration/tool-filter-service.d.ts.map +1 -0
- package/dist/src/tool-filter/integration/tool-filter-service.js +68 -0
- package/dist/src/tool-filter/integration/tool-filter-service.js.map +1 -0
- package/dist/src/tool-filter/operation/operation-classifier.d.ts +20 -0
- package/dist/src/tool-filter/operation/operation-classifier.d.ts.map +1 -0
- package/dist/src/tool-filter/operation/operation-classifier.js +26 -0
- package/dist/src/tool-filter/operation/operation-classifier.js.map +1 -0
- package/dist/src/tool-filter/operation/operation-detector.d.ts +30 -0
- package/dist/src/tool-filter/operation/operation-detector.d.ts.map +1 -0
- package/dist/src/tool-filter/operation/operation-detector.js +96 -0
- package/dist/src/tool-filter/operation/operation-detector.js.map +1 -0
- package/dist/src/tool-filter/operation/operation-resolver.d.ts +22 -0
- package/dist/src/tool-filter/operation/operation-resolver.d.ts.map +1 -0
- package/dist/src/tool-filter/operation/operation-resolver.js +32 -0
- package/dist/src/tool-filter/operation/operation-resolver.js.map +1 -0
- package/dist/src/tool-filter/regex/regex-compiler.d.ts +22 -0
- package/dist/src/tool-filter/regex/regex-compiler.d.ts.map +1 -0
- package/dist/src/tool-filter/regex/regex-compiler.js +56 -0
- package/dist/src/tool-filter/regex/regex-compiler.js.map +1 -0
- package/dist/src/tool-filter/regex/regex-validator.d.ts +24 -0
- package/dist/src/tool-filter/regex/regex-validator.d.ts.map +1 -0
- package/dist/src/tool-filter/regex/regex-validator.js +58 -0
- package/dist/src/tool-filter/regex/regex-validator.js.map +1 -0
- package/dist/src/tool-filter/types.d.ts +92 -0
- package/dist/src/tool-filter/types.d.ts.map +1 -0
- package/dist/src/tool-filter/types.js +5 -0
- package/dist/src/tool-filter/types.js.map +1 -0
- package/dist/src/tool-filter/utils.d.ts +11 -0
- package/dist/src/tool-filter/utils.d.ts.map +1 -0
- package/dist/src/tool-filter/utils.js +13 -0
- package/dist/src/tool-filter/utils.js.map +1 -0
- package/dist/src/{composite-executor.d.ts → tooling/composite-executor.d.ts} +3 -3
- package/dist/src/tooling/composite-executor.d.ts.map +1 -0
- package/dist/src/{composite-executor.js → tooling/composite-executor.js} +1 -1
- package/dist/src/tooling/composite-executor.js.map +1 -0
- package/dist/src/{dag-executor.d.ts → tooling/dag-executor.d.ts} +1 -1
- package/dist/src/tooling/dag-executor.d.ts.map +1 -0
- package/dist/src/tooling/dag-executor.js.map +1 -0
- package/dist/src/{proxy-executor.d.ts → tooling/proxy-executor.d.ts} +2 -2
- package/dist/src/tooling/proxy-executor.d.ts.map +1 -0
- package/dist/src/{proxy-executor.js → tooling/proxy-executor.js} +8 -1
- package/dist/src/tooling/proxy-executor.js.map +1 -0
- package/dist/src/{tool-generator.d.ts → tooling/tool-generator.d.ts} +4 -3
- package/dist/src/tooling/tool-generator.d.ts.map +1 -0
- package/dist/src/{tool-generator.js → tooling/tool-generator.js} +23 -7
- package/dist/src/tooling/tool-generator.js.map +1 -0
- package/dist/src/{http-client-factory.d.ts → transport/http-client-factory.d.ts} +4 -1
- package/dist/src/transport/http-client-factory.d.ts.map +1 -0
- package/dist/src/{http-client-factory.js → transport/http-client-factory.js} +13 -3
- package/dist/src/transport/http-client-factory.js.map +1 -0
- package/dist/src/transport/http-transport-config.d.ts +6 -0
- package/dist/src/transport/http-transport-config.d.ts.map +1 -0
- package/dist/src/transport/http-transport-config.js +62 -0
- package/dist/src/transport/http-transport-config.js.map +1 -0
- package/dist/src/{http-transport.d.ts → transport/http-transport.d.ts} +72 -14
- package/dist/src/transport/http-transport.d.ts.map +1 -0
- package/dist/src/{http-transport.js → transport/http-transport.js} +1166 -493
- package/dist/src/transport/http-transport.js.map +1 -0
- package/dist/src/{interceptors.d.ts → transport/interceptors.d.ts} +6 -2
- package/dist/src/transport/interceptors.d.ts.map +1 -0
- package/dist/src/{interceptors.js → transport/interceptors.js} +72 -41
- package/dist/src/transport/interceptors.js.map +1 -0
- package/dist/src/types/http-transport.d.ts +25 -0
- package/dist/src/types/http-transport.d.ts.map +1 -1
- package/dist/src/types/profile.d.ts +13 -1
- package/dist/src/types/profile.d.ts.map +1 -1
- package/dist/src/validation/argument-normalizer.d.ts +6 -0
- package/dist/src/validation/argument-normalizer.d.ts.map +1 -0
- package/dist/src/validation/argument-normalizer.js +70 -0
- package/dist/src/validation/argument-normalizer.js.map +1 -0
- package/dist/src/validation/jsonrpc-validator.d.ts.map +1 -0
- package/dist/src/validation/jsonrpc-validator.js.map +1 -0
- package/dist/src/{schema-validator.d.ts → validation/schema-validator.d.ts} +2 -2
- package/dist/src/validation/schema-validator.d.ts.map +1 -0
- package/dist/src/validation/schema-validator.js.map +1 -0
- package/dist/src/validation/validation-utils.d.ts.map +1 -0
- package/dist/src/validation/validation-utils.js.map +1 -0
- package/package.json +9 -3
- package/profile-schema.json +63 -3
- package/profiles/gitlab/developer-profile-oauth.json +1520 -0
- package/profiles/gitlab/developer-profile-oauth.test.json +3432 -0
- package/profiles/gitlab/openapi.yaml +6891 -0
- package/profiles/n8n/openapi.yaml +2441 -0
- package/profiles/n8n/profile-optimized.json +965 -0
- package/profiles/n8n/profile-optimized.test.json +1078 -0
- package/profiles/n8n/profile.json +1033 -0
- package/profiles/n8n/profile.test.json +983 -0
- package/profiles/n8n-nodes/openapi.yaml +24 -0
- package/profiles/n8n-nodes/profile-nodes.json +44 -0
- package/profiles/n8n-nodes/profile-nodes.test.json +91 -0
- package/profiles/semgrep/openapi.yaml +4706 -0
- package/profiles/semgrep/profile.json +692 -0
- package/profiles/semgrep/profile.test.json +471 -0
- package/profiles/youtrack/openapi.json +16976 -0
- package/profiles/youtrack/profile.json +608 -0
- package/profiles/youtrack/profile.test.json +1926 -0
- package/dist/src/composite-executor.d.ts.map +0 -1
- package/dist/src/composite-executor.js.map +0 -1
- package/dist/src/constants.d.ts.map +0 -1
- package/dist/src/constants.js.map +0 -1
- package/dist/src/dag-executor.d.ts.map +0 -1
- package/dist/src/dag-executor.js.map +0 -1
- package/dist/src/errors.d.ts.map +0 -1
- package/dist/src/errors.js.map +0 -1
- package/dist/src/http-client-factory.d.ts.map +0 -1
- package/dist/src/http-client-factory.js.map +0 -1
- package/dist/src/http-transport.d.ts.map +0 -1
- package/dist/src/http-transport.js.map +0 -1
- package/dist/src/interceptors.d.ts.map +0 -1
- package/dist/src/interceptors.js.map +0 -1
- package/dist/src/jsonrpc-validator.d.ts.map +0 -1
- package/dist/src/jsonrpc-validator.js.map +0 -1
- package/dist/src/logger.d.ts.map +0 -1
- package/dist/src/logger.js.map +0 -1
- package/dist/src/mcp-server.d.ts.map +0 -1
- package/dist/src/mcp-server.js.map +0 -1
- package/dist/src/metrics.d.ts.map +0 -1
- package/dist/src/metrics.js.map +0 -1
- package/dist/src/naming-warnings.d.ts.map +0 -1
- package/dist/src/naming-warnings.js.map +0 -1
- package/dist/src/naming.d.ts.map +0 -1
- package/dist/src/naming.js.map +0 -1
- package/dist/src/oauth-provider.d.ts.map +0 -1
- package/dist/src/oauth-provider.js.map +0 -1
- package/dist/src/openapi-parser.d.ts.map +0 -1
- package/dist/src/openapi-parser.js.map +0 -1
- package/dist/src/profile-loader.d.ts.map +0 -1
- package/dist/src/profile-loader.js.map +0 -1
- package/dist/src/proxy-executor.d.ts.map +0 -1
- package/dist/src/proxy-executor.js.map +0 -1
- package/dist/src/schema-validator.d.ts.map +0 -1
- package/dist/src/schema-validator.js.map +0 -1
- package/dist/src/testing/fixtures.d.ts +0 -684
- package/dist/src/testing/fixtures.d.ts.map +0 -1
- package/dist/src/testing/fixtures.js +0 -528
- package/dist/src/testing/fixtures.js.map +0 -1
- package/dist/src/testing/mock-gitlab-server.d.ts +0 -43
- package/dist/src/testing/mock-gitlab-server.d.ts.map +0 -1
- package/dist/src/testing/mock-gitlab-server.js +0 -1026
- package/dist/src/testing/mock-gitlab-server.js.map +0 -1
- package/dist/src/testing/mock-semgrep-server.d.ts +0 -32
- package/dist/src/testing/mock-semgrep-server.d.ts.map +0 -1
- package/dist/src/testing/mock-semgrep-server.js +0 -213
- package/dist/src/testing/mock-semgrep-server.js.map +0 -1
- package/dist/src/testing/mock-youtrack-server.d.ts +0 -11
- package/dist/src/testing/mock-youtrack-server.d.ts.map +0 -1
- package/dist/src/testing/mock-youtrack-server.js +0 -152
- package/dist/src/testing/mock-youtrack-server.js.map +0 -1
- package/dist/src/tool-generator.d.ts.map +0 -1
- package/dist/src/tool-generator.js.map +0 -1
- package/dist/src/validation-utils.d.ts.map +0 -1
- package/dist/src/validation-utils.js.map +0 -1
- /package/dist/src/{naming-warnings.d.ts → core/naming-warnings.d.ts} +0 -0
- /package/dist/src/{naming-warnings.js → core/naming-warnings.js} +0 -0
- /package/dist/src/{naming.d.ts → core/naming.d.ts} +0 -0
- /package/dist/src/{naming.js → core/naming.js} +0 -0
- /package/dist/src/{dag-executor.js → tooling/dag-executor.js} +0 -0
- /package/dist/src/{jsonrpc-validator.d.ts → validation/jsonrpc-validator.d.ts} +0 -0
- /package/dist/src/{jsonrpc-validator.js → validation/jsonrpc-validator.js} +0 -0
- /package/dist/src/{schema-validator.js → validation/schema-validator.js} +0 -0
- /package/dist/src/{validation-utils.d.ts → validation/validation-utils.d.ts} +0 -0
- /package/dist/src/{validation-utils.js → validation/validation-utils.js} +0 -0
|
@@ -7,21 +7,41 @@
|
|
|
7
7
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
9
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
-
import { OpenAPIParser } from '
|
|
11
|
-
import { ProfileLoader } from '
|
|
12
|
-
import { ToolGenerator } from '
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
10
|
+
import { OpenAPIParser } from '../openapi/openapi-parser.js';
|
|
11
|
+
import { ProfileLoader } from '../profile/profile-loader.js';
|
|
12
|
+
import { ToolGenerator } from '../tooling/tool-generator.js';
|
|
13
|
+
import { applyParameterDefaults, normalizeArguments } from '../validation/argument-normalizer.js';
|
|
14
|
+
import { CompositeExecutor } from '../tooling/composite-executor.js';
|
|
15
|
+
import { ProxyDownloadExecutor } from '../tooling/proxy-executor.js';
|
|
16
|
+
import { enforceFiltering, parseFilteringHeader } from '../core/filtering.js';
|
|
17
|
+
import { ConfigurationError, OperationNotFoundError, ResourceNotFoundError, ValidationError, AuthenticationError, AuthorizationError, RateLimitError, NetworkError, generateCorrelationId } from '../core/errors.js';
|
|
18
|
+
import { OAUTH_RATE_LIMIT } from '../core/constants.js';
|
|
19
|
+
import { HttpClientFactory } from '../transport/http-client-factory.js';
|
|
20
|
+
import { SchemaValidator } from '../validation/schema-validator.js';
|
|
21
|
+
import { ConsoleLogger, JsonLogger } from '../core/logger.js';
|
|
22
|
+
import { isInitializeRequest, isToolCallRequest } from '../validation/jsonrpc-validator.js';
|
|
23
|
+
import { generateNameWarnings } from '../core/naming-warnings.js';
|
|
24
|
+
import { NamingStrategy } from '../core/naming.js';
|
|
25
|
+
import { isSafePropertyName } from '../validation/validation-utils.js';
|
|
26
|
+
import { ToolFilterService, EnvConfigParser, HeaderConfigParser, RegexCompiler, RegexValidator, OperationClassifier, OpenAPIOperationResolver, OperationDetector, applySessionToolFilter, } from '../tool-filter/index.js';
|
|
27
|
+
import { buildHttpTransportBaseConfig } from '../transport/http-transport-config.js';
|
|
24
28
|
export class MCPServer {
|
|
29
|
+
/**
|
|
30
|
+
* Execute a tools/call request via the JSON-RPC handler.
|
|
31
|
+
* Intended for internal use and tests to avoid accessing private methods.
|
|
32
|
+
*/
|
|
33
|
+
async callToolRpc(name, args, sessionId, requestId = 1) {
|
|
34
|
+
const message = {
|
|
35
|
+
jsonrpc: '2.0',
|
|
36
|
+
id: requestId,
|
|
37
|
+
method: 'tools/call',
|
|
38
|
+
params: {
|
|
39
|
+
name,
|
|
40
|
+
arguments: args,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return this.handleToolCall(message, sessionId);
|
|
44
|
+
}
|
|
25
45
|
/**
|
|
26
46
|
* Filter response payload to include only specified fields.
|
|
27
47
|
*
|
|
@@ -46,22 +66,17 @@ export class MCPServer {
|
|
|
46
66
|
return root;
|
|
47
67
|
}
|
|
48
68
|
mergeFieldSelector(target, selector) {
|
|
49
|
-
const
|
|
69
|
+
const parsed = this.parseFieldSelector(selector);
|
|
70
|
+
const baseName = parsed.baseName;
|
|
50
71
|
if (!baseName)
|
|
51
72
|
return;
|
|
52
73
|
if (!isSafePropertyName(baseName))
|
|
53
74
|
return;
|
|
54
|
-
|
|
55
|
-
if (openParen === -1) {
|
|
56
|
-
target[baseName] = true;
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
const closeParen = selector.lastIndexOf(')');
|
|
60
|
-
if (closeParen === -1 || closeParen <= openParen) {
|
|
75
|
+
if (!parsed.inner) {
|
|
61
76
|
target[baseName] = true;
|
|
62
77
|
return;
|
|
63
78
|
}
|
|
64
|
-
const inner =
|
|
79
|
+
const inner = parsed.inner;
|
|
65
80
|
const subSelectors = this.splitTopLevel(inner);
|
|
66
81
|
const subTree = Object.create(null);
|
|
67
82
|
for (const sub of subSelectors) {
|
|
@@ -76,6 +91,59 @@ export class MCPServer {
|
|
|
76
91
|
}
|
|
77
92
|
this.mergeSelectionTrees(existing, subTree);
|
|
78
93
|
}
|
|
94
|
+
parseFieldSelector(selector) {
|
|
95
|
+
const trimmed = selector.trim();
|
|
96
|
+
if (!trimmed)
|
|
97
|
+
return { baseName: '' };
|
|
98
|
+
if (trimmed.startsWith('"')) {
|
|
99
|
+
const parsedQuoted = this.parseQuotedBase(trimmed);
|
|
100
|
+
if (parsedQuoted) {
|
|
101
|
+
const { baseName, rest } = parsedQuoted;
|
|
102
|
+
const remaining = rest.trim();
|
|
103
|
+
if (!remaining) {
|
|
104
|
+
return { baseName };
|
|
105
|
+
}
|
|
106
|
+
if (remaining.startsWith('(') && remaining.endsWith(')')) {
|
|
107
|
+
const inner = remaining.slice(1, -1).trim();
|
|
108
|
+
return inner ? { baseName, inner } : { baseName };
|
|
109
|
+
}
|
|
110
|
+
return { baseName };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const openParen = trimmed.indexOf('(');
|
|
114
|
+
if (openParen === -1) {
|
|
115
|
+
return { baseName: trimmed };
|
|
116
|
+
}
|
|
117
|
+
const closeParen = trimmed.lastIndexOf(')');
|
|
118
|
+
if (closeParen === -1 || closeParen <= openParen) {
|
|
119
|
+
return { baseName: trimmed.slice(0, openParen).trim() };
|
|
120
|
+
}
|
|
121
|
+
const baseName = trimmed.slice(0, openParen).trim();
|
|
122
|
+
const inner = trimmed.slice(openParen + 1, closeParen).trim();
|
|
123
|
+
return inner ? { baseName, inner } : { baseName };
|
|
124
|
+
}
|
|
125
|
+
parseQuotedBase(input) {
|
|
126
|
+
let escaped = false;
|
|
127
|
+
let base = '';
|
|
128
|
+
for (let i = 1; i < input.length; i += 1) {
|
|
129
|
+
const ch = input[i];
|
|
130
|
+
if (escaped) {
|
|
131
|
+
base += ch;
|
|
132
|
+
escaped = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === '\\') {
|
|
136
|
+
escaped = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === '"') {
|
|
140
|
+
const rest = input.slice(i + 1);
|
|
141
|
+
return { baseName: base, rest };
|
|
142
|
+
}
|
|
143
|
+
base += ch;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
79
147
|
mergeSelectionTrees(target, incoming) {
|
|
80
148
|
for (const [key, val] of Object.entries(incoming)) {
|
|
81
149
|
if (!isSafePropertyName(key))
|
|
@@ -96,12 +164,31 @@ export class MCPServer {
|
|
|
96
164
|
const result = [];
|
|
97
165
|
let depth = 0;
|
|
98
166
|
let current = '';
|
|
167
|
+
let inQuote = false;
|
|
168
|
+
let escaped = false;
|
|
99
169
|
for (const ch of input) {
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
170
|
+
if (escaped) {
|
|
171
|
+
current += ch;
|
|
172
|
+
escaped = false;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (ch === '\\' && inQuote) {
|
|
176
|
+
current += ch;
|
|
177
|
+
escaped = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (ch === '"') {
|
|
181
|
+
inQuote = !inQuote;
|
|
182
|
+
current += ch;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!inQuote) {
|
|
186
|
+
if (ch === '(')
|
|
187
|
+
depth += 1;
|
|
188
|
+
if (ch === ')')
|
|
189
|
+
depth = Math.max(0, depth - 1);
|
|
190
|
+
}
|
|
191
|
+
if (!inQuote && ch === ',' && depth === 0) {
|
|
105
192
|
const trimmed = current.trim();
|
|
106
193
|
if (trimmed)
|
|
107
194
|
result.push(trimmed);
|
|
@@ -177,6 +264,9 @@ export class MCPServer {
|
|
|
177
264
|
if (error instanceof OperationNotFoundError) {
|
|
178
265
|
return `Operation not found: ${error.message} (correlation ID: ${correlationId})`;
|
|
179
266
|
}
|
|
267
|
+
if (error instanceof ResourceNotFoundError) {
|
|
268
|
+
return `${error.message} (correlation ID: ${correlationId})`;
|
|
269
|
+
}
|
|
180
270
|
// Configuration errors - safe to show (helps admin fix setup)
|
|
181
271
|
if (error instanceof ConfigurationError) {
|
|
182
272
|
return `Configuration error: ${error.message} (correlation ID: ${correlationId})`;
|
|
@@ -223,6 +313,7 @@ export class MCPServer {
|
|
|
223
313
|
// Check if we should warn about long names
|
|
224
314
|
this.checkToolNameLengths();
|
|
225
315
|
}
|
|
316
|
+
this.applyGlobalToolFiltering();
|
|
226
317
|
// Re-create logger with auth config for token redaction
|
|
227
318
|
const authConfigs = this.getAuthConfigs();
|
|
228
319
|
if (authConfigs.length > 0) {
|
|
@@ -239,8 +330,8 @@ export class MCPServer {
|
|
|
239
330
|
const envAuthConfig = this.getEnvBackedAuthConfig();
|
|
240
331
|
const envVarName = envAuthConfig?.value_from_env;
|
|
241
332
|
const envToken = envVarName ? process.env[envVarName] : undefined;
|
|
242
|
-
if (envAuthConfig && envToken) {
|
|
243
|
-
// Token available in env - create global client
|
|
333
|
+
if ((envAuthConfig && envToken) || authConfigs.length === 0) {
|
|
334
|
+
// Token available in env (stdio) or no auth required - create global client
|
|
244
335
|
const httpClient = this.httpClientFactory.createGlobalClient({
|
|
245
336
|
profile: this.profile,
|
|
246
337
|
baseUrl,
|
|
@@ -346,6 +437,59 @@ export class MCPServer {
|
|
|
346
437
|
const oauthConfig = configs.find(c => c.type === 'oauth');
|
|
347
438
|
return oauthConfig?.oauth_config;
|
|
348
439
|
}
|
|
440
|
+
buildOAuthConfigWithAllowedRedirectHosts(oauthConfig) {
|
|
441
|
+
if (!oauthConfig) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
...oauthConfig,
|
|
446
|
+
allowed_redirect_hosts: oauthConfig.allowed_redirect_hosts
|
|
447
|
+
|| (process.env.MCP4_ALLOWED_ORIGINS
|
|
448
|
+
? this.extractHostsFromOrigins(process.env.MCP4_ALLOWED_ORIGINS)
|
|
449
|
+
: undefined),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
getProfileIdValue() {
|
|
453
|
+
if (!this.profile) {
|
|
454
|
+
throw new ConfigurationError('Profile not initialized. Call initialize() first.');
|
|
455
|
+
}
|
|
456
|
+
const profileId = this.profile.profile_id?.trim() || this.profile.profile_name;
|
|
457
|
+
if (!profileId) {
|
|
458
|
+
throw new ConfigurationError('Profile is missing profile_id and profile_name.');
|
|
459
|
+
}
|
|
460
|
+
return profileId;
|
|
461
|
+
}
|
|
462
|
+
getOAuthRateLimitConfig() {
|
|
463
|
+
const authConfigs = this.getAuthConfigs();
|
|
464
|
+
const oauthAuthConfig = authConfigs.find(c => c.type === 'oauth');
|
|
465
|
+
const oauthRateLimit = oauthAuthConfig?.oauth_rate_limit;
|
|
466
|
+
const max = oauthRateLimit?.max_requests
|
|
467
|
+
|| parseInt(process.env.MCP4_OAUTH_RATE_LIMIT_MAX || String(OAUTH_RATE_LIMIT.MAX_REQUESTS), 10);
|
|
468
|
+
const windowMs = oauthRateLimit?.window_ms
|
|
469
|
+
|| parseInt(process.env.MCP4_OAUTH_RATE_LIMIT_WINDOW_MS || String(OAUTH_RATE_LIMIT.WINDOW_MS), 10);
|
|
470
|
+
return { max, windowMs };
|
|
471
|
+
}
|
|
472
|
+
getHttpProfileContext() {
|
|
473
|
+
if (!this.profile) {
|
|
474
|
+
throw new ConfigurationError('Profile not initialized. Call initialize() first.');
|
|
475
|
+
}
|
|
476
|
+
const authConfigs = this.getAuthConfigs();
|
|
477
|
+
const baseUrl = this.getBaseUrl();
|
|
478
|
+
const oauthConfig = this.buildOAuthConfigWithAllowedRedirectHosts(this.getOAuthConfig());
|
|
479
|
+
const resourceMetadata = this.parser.getResourceMetadata();
|
|
480
|
+
const oauthRateLimit = this.getOAuthRateLimitConfig();
|
|
481
|
+
return {
|
|
482
|
+
profileId: this.getProfileIdValue(),
|
|
483
|
+
oauthConfig,
|
|
484
|
+
authConfigs,
|
|
485
|
+
baseUrl,
|
|
486
|
+
rateLimitOAuthMax: oauthRateLimit.max,
|
|
487
|
+
rateLimitOAuthWindowMs: oauthRateLimit.windowMs,
|
|
488
|
+
resourceName: this.profile.resource_name || resourceMetadata.name || 'MCP Server',
|
|
489
|
+
resourceDocumentation: this.profile.resource_documentation || resourceMetadata.documentation,
|
|
490
|
+
parser: this.parser,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
349
493
|
/**
|
|
350
494
|
* Extract hostnames from origin patterns for OAuth redirect validation
|
|
351
495
|
* e.g., "http://localhost:*,https://app.example.com" -> ["localhost", "app.example.com"]
|
|
@@ -385,7 +529,7 @@ export class MCPServer {
|
|
|
385
529
|
/**
|
|
386
530
|
* Get or create HTTP client for session
|
|
387
531
|
*/
|
|
388
|
-
async getHttpClientForSession(sessionId) {
|
|
532
|
+
async getHttpClientForSession(sessionId, profileId) {
|
|
389
533
|
if (!sessionId) {
|
|
390
534
|
// Fallback to global client for stdio transport
|
|
391
535
|
if (!this.httpClientFactory.hasGlobalClient()) {
|
|
@@ -408,7 +552,7 @@ export class MCPServer {
|
|
|
408
552
|
throw new ConfigurationError('Profile not initialized. Call initialize() first.');
|
|
409
553
|
}
|
|
410
554
|
// Get auth token from session (ensures token is valid/refreshed)
|
|
411
|
-
const authToken = await this.getAuthTokenFromSession(sessionId);
|
|
555
|
+
const authToken = await this.getAuthTokenFromSession(sessionId, profileId);
|
|
412
556
|
// Create or get session client using factory
|
|
413
557
|
return this.httpClientFactory.getOrCreateSessionClient(sessionId, {
|
|
414
558
|
profile: this.profile,
|
|
@@ -420,7 +564,7 @@ export class MCPServer {
|
|
|
420
564
|
* Get auth token from HTTP transport session
|
|
421
565
|
* Ensures token is valid (refreshes if expired) before returning
|
|
422
566
|
*/
|
|
423
|
-
async getAuthTokenFromSession(sessionId) {
|
|
567
|
+
async getAuthTokenFromSession(sessionId, profileId) {
|
|
424
568
|
// Early return if sessionId is missing/empty
|
|
425
569
|
// Prevents misleading warn logs with empty sessionId
|
|
426
570
|
if (!sessionId) {
|
|
@@ -430,23 +574,24 @@ export class MCPServer {
|
|
|
430
574
|
return undefined;
|
|
431
575
|
}
|
|
432
576
|
// Ensure token is valid (refresh if expired)
|
|
433
|
-
const
|
|
577
|
+
const effectiveProfileId = profileId || this.getProfileIdValue();
|
|
578
|
+
const isValid = await this.httpTransport.ensureValidSessionToken(effectiveProfileId, sessionId);
|
|
434
579
|
if (!isValid) {
|
|
435
|
-
this.logger.warn('Session token validation/refresh failed', { sessionId });
|
|
580
|
+
this.logger.warn('Session token validation/refresh failed', { profileId: effectiveProfileId, sessionId });
|
|
436
581
|
// Still return token if available - let the API call fail with proper error
|
|
437
582
|
}
|
|
438
583
|
// Use public API instead of type casting
|
|
439
|
-
return this.httpTransport.getSessionToken(sessionId);
|
|
584
|
+
return this.httpTransport.getSessionToken(effectiveProfileId, sessionId);
|
|
440
585
|
}
|
|
441
586
|
/**
|
|
442
587
|
* Cleanup HTTP client for destroyed session
|
|
443
588
|
*
|
|
444
589
|
* Why: Prevent memory leak - sessions expire but cached clients stay forever
|
|
445
590
|
*/
|
|
446
|
-
cleanupSessionClient(sessionId) {
|
|
591
|
+
cleanupSessionClient(profileId, sessionId) {
|
|
447
592
|
const removed = this.httpClientFactory.cleanupSessionClient(sessionId);
|
|
448
593
|
if (removed) {
|
|
449
|
-
this.logger.info('Cleaned up session HTTP client', { sessionId });
|
|
594
|
+
this.logger.info('Cleaned up session HTTP client', { profileId, sessionId });
|
|
450
595
|
}
|
|
451
596
|
}
|
|
452
597
|
/**
|
|
@@ -480,7 +625,8 @@ export class MCPServer {
|
|
|
480
625
|
if (!toolDef) {
|
|
481
626
|
throw new OperationNotFoundError(request.params.name);
|
|
482
627
|
}
|
|
483
|
-
const
|
|
628
|
+
const rawArgs = request.params.arguments || {};
|
|
629
|
+
const args = applyParameterDefaults(toolDef, rawArgs);
|
|
484
630
|
// Validate arguments
|
|
485
631
|
this.toolGenerator.validateArguments(toolDef, args);
|
|
486
632
|
// Execute composite or simple tool
|
|
@@ -530,15 +676,16 @@ export class MCPServer {
|
|
|
530
676
|
* Why separate: Simple tools map directly to single OpenAPI operation.
|
|
531
677
|
* No result aggregation needed.
|
|
532
678
|
*/
|
|
533
|
-
async executeSimpleTool(toolDef, args, sessionId) {
|
|
679
|
+
async executeSimpleTool(toolDef, args, sessionId, profileId) {
|
|
680
|
+
const normalizedArgs = normalizeArguments(toolDef, args);
|
|
534
681
|
this.logger.debug('Executing simple tool', {
|
|
535
682
|
toolName: toolDef.name,
|
|
536
|
-
action:
|
|
537
|
-
resourceType:
|
|
683
|
+
action: normalizedArgs['action'],
|
|
684
|
+
resourceType: normalizedArgs['resource_type'],
|
|
538
685
|
sessionId
|
|
539
686
|
});
|
|
540
687
|
// Get operation definition (can be string or ProxyDownloadOperation)
|
|
541
|
-
const operationDef = this.toolGenerator.getOperationDefinition(toolDef,
|
|
688
|
+
const operationDef = this.toolGenerator.getOperationDefinition(toolDef, normalizedArgs);
|
|
542
689
|
if (!operationDef) {
|
|
543
690
|
throw new ValidationError(`Could not map tool action to operation`, {
|
|
544
691
|
toolName: toolDef.name,
|
|
@@ -549,7 +696,7 @@ export class MCPServer {
|
|
|
549
696
|
}
|
|
550
697
|
// Check if this is a proxy download operation
|
|
551
698
|
if (typeof operationDef === 'object' && operationDef.type === 'proxy_download') {
|
|
552
|
-
return this.executeProxyDownload(operationDef,
|
|
699
|
+
return this.executeProxyDownload(operationDef, normalizedArgs, sessionId, profileId);
|
|
553
700
|
}
|
|
554
701
|
// Regular string operation
|
|
555
702
|
const operationId = operationDef;
|
|
@@ -558,9 +705,9 @@ export class MCPServer {
|
|
|
558
705
|
throw new OperationNotFoundError(operationId);
|
|
559
706
|
}
|
|
560
707
|
// Build request
|
|
561
|
-
const path = this.resolvePath(operation.path,
|
|
562
|
-
const queryParams = this.extractQueryParams(operation,
|
|
563
|
-
const body = this.extractBody(operation,
|
|
708
|
+
const path = this.resolvePath(operation.path, normalizedArgs);
|
|
709
|
+
const queryParams = this.extractQueryParams(operation, normalizedArgs);
|
|
710
|
+
const body = this.extractBody(operation, normalizedArgs, toolDef);
|
|
564
711
|
this.logger.debug('Executing HTTP request', {
|
|
565
712
|
operationId,
|
|
566
713
|
method: operation.method,
|
|
@@ -579,9 +726,9 @@ export class MCPServer {
|
|
|
579
726
|
}
|
|
580
727
|
}
|
|
581
728
|
// Execute with session-specific client
|
|
582
|
-
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
729
|
+
const httpClient = await this.getHttpClientForSession(sessionId, profileId);
|
|
583
730
|
// Set fields parameter if response_fields are configured for this action AND enabled
|
|
584
|
-
const action =
|
|
731
|
+
const action = normalizedArgs.action;
|
|
585
732
|
if (toolDef.send_response_fields_as_param && toolDef.response_fields && action && toolDef.response_fields[action]) {
|
|
586
733
|
const fields = toolDef.response_fields[action];
|
|
587
734
|
queryParams.fields = fields.join(',');
|
|
@@ -594,7 +741,7 @@ export class MCPServer {
|
|
|
594
741
|
// Apply response field filtering if configured
|
|
595
742
|
let result = response.body;
|
|
596
743
|
if (toolDef.response_fields) {
|
|
597
|
-
const action =
|
|
744
|
+
const action = normalizedArgs.action;
|
|
598
745
|
if (action && toolDef.response_fields[action]) {
|
|
599
746
|
const fields = toolDef.response_fields[action];
|
|
600
747
|
result = this.filterFields(result, fields);
|
|
@@ -608,7 +755,7 @@ export class MCPServer {
|
|
|
608
755
|
* Why: Some APIs return authenticated URLs that LLMs cannot fetch directly.
|
|
609
756
|
* This proxies the download through the MCP server.
|
|
610
757
|
*/
|
|
611
|
-
async executeProxyDownload(operation, args, sessionId) {
|
|
758
|
+
async executeProxyDownload(operation, args, sessionId, profileId) {
|
|
612
759
|
this.logger.debug('Executing proxy download', {
|
|
613
760
|
metadataEndpoint: operation.metadata_endpoint,
|
|
614
761
|
urlField: operation.url_field,
|
|
@@ -634,7 +781,7 @@ export class MCPServer {
|
|
|
634
781
|
};
|
|
635
782
|
}
|
|
636
783
|
// Get auth credentials for download
|
|
637
|
-
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
784
|
+
const httpClient = await this.getHttpClientForSession(sessionId, profileId);
|
|
638
785
|
const authCredentials = httpClient.getAuthCredentials();
|
|
639
786
|
// Execute proxy download
|
|
640
787
|
const proxyExecutor = new ProxyDownloadExecutor(httpClient, this.logger);
|
|
@@ -735,17 +882,47 @@ export class MCPServer {
|
|
|
735
882
|
pathOrQuery.add(param.name);
|
|
736
883
|
}
|
|
737
884
|
}
|
|
738
|
-
// Get body schema properties to check if path/query params should also be in body
|
|
885
|
+
// Get body schema and properties to check if path/query params should also be in body
|
|
886
|
+
let bodySchema;
|
|
739
887
|
const bodySchemaProps = new Set();
|
|
740
888
|
if (operation.requestBody?.content) {
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
889
|
+
// Prefer application/json but accept any schema present
|
|
890
|
+
const jsonSchema = operation.requestBody.content['application/json']?.schema;
|
|
891
|
+
bodySchema = jsonSchema;
|
|
892
|
+
if (!bodySchema) {
|
|
893
|
+
for (const mediaType of Object.values(operation.requestBody.content)) {
|
|
894
|
+
if (mediaType.schema) {
|
|
895
|
+
bodySchema = mediaType.schema;
|
|
896
|
+
break;
|
|
746
897
|
}
|
|
747
898
|
}
|
|
748
899
|
}
|
|
900
|
+
if (bodySchema?.type === 'object' && bodySchema.properties) {
|
|
901
|
+
for (const propName of Object.keys(bodySchema.properties)) {
|
|
902
|
+
bodySchemaProps.add(propName);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// Root array body support
|
|
907
|
+
if (bodySchema?.type === 'array') {
|
|
908
|
+
const explicit = args['body'] ?? args['items'];
|
|
909
|
+
if (explicit !== undefined) {
|
|
910
|
+
return explicit;
|
|
911
|
+
}
|
|
912
|
+
const arrayCandidates = [];
|
|
913
|
+
for (const [key, value] of Object.entries(args)) {
|
|
914
|
+
if (metadata.has(key))
|
|
915
|
+
continue;
|
|
916
|
+
if (pathOrQuery.has(key))
|
|
917
|
+
continue;
|
|
918
|
+
if (Array.isArray(value)) {
|
|
919
|
+
arrayCandidates.push(value);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (arrayCandidates.length === 1) {
|
|
923
|
+
return arrayCandidates[0];
|
|
924
|
+
}
|
|
925
|
+
return undefined;
|
|
749
926
|
}
|
|
750
927
|
const body = {};
|
|
751
928
|
let hasBody = false;
|
|
@@ -780,76 +957,26 @@ export class MCPServer {
|
|
|
780
957
|
* and resumability for reliable communication over HTTP.
|
|
781
958
|
*/
|
|
782
959
|
async runHttp(host, port) {
|
|
783
|
-
const { HttpTransport } = await import('
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (oauthConfig) {
|
|
960
|
+
const { HttpTransport } = await import('../transport/http-transport.js');
|
|
961
|
+
const profileContext = this.getHttpProfileContext();
|
|
962
|
+
if (profileContext.oauthConfig) {
|
|
787
963
|
this.logger.info('OAuth authentication enabled for HTTP transport');
|
|
788
964
|
}
|
|
789
|
-
|
|
790
|
-
const authConfigs = this.getAuthConfigs();
|
|
791
|
-
const baseUrl = this.getBaseUrl();
|
|
792
|
-
// Extract OAuth rate limit from profile (if configured)
|
|
793
|
-
const oauthAuthConfig = authConfigs.find(c => c.type === 'oauth');
|
|
794
|
-
const oauthRateLimit = oauthAuthConfig?.oauth_rate_limit;
|
|
795
|
-
// Extract resource metadata from OpenAPI spec or profile
|
|
796
|
-
const resourceMetadata = this.parser.getResourceMetadata();
|
|
965
|
+
const baseConfig = buildHttpTransportBaseConfig(host, port);
|
|
797
966
|
const config = {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
heartbeatEnabled: process.env.MCP4_HEARTBEAT_ENABLED === 'true',
|
|
802
|
-
heartbeatIntervalMs: parseInt(process.env.MCP4_HEARTBEAT_INTERVAL_MS || String(TIMEOUTS.HEARTBEAT_INTERVAL_MS), 10),
|
|
803
|
-
metricsEnabled: process.env.MCP4_METRICS_ENABLED === 'true',
|
|
804
|
-
metricsPath: process.env.MCP4_METRICS_PATH || '/metrics',
|
|
805
|
-
allowedOrigins: process.env.MCP4_ALLOWED_ORIGINS
|
|
806
|
-
? process.env.MCP4_ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
|
807
|
-
: undefined,
|
|
808
|
-
rateLimitEnabled: process.env.MCP4_HTTP_RATE_LIMIT_ENABLED !== 'false', // default: true
|
|
809
|
-
rateLimitWindowMs: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_WINDOW_MS || String(TIMEOUTS.RATE_LIMIT_WINDOW_MS), 10),
|
|
810
|
-
rateLimitMaxRequests: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_MAX_REQUESTS || '100', 10),
|
|
811
|
-
rateLimitMetricsMax: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_METRICS_MAX || '10', 10),
|
|
967
|
+
...baseConfig,
|
|
968
|
+
profileRoutingEnabled: false,
|
|
969
|
+
defaultProfileId: profileContext.profileId,
|
|
812
970
|
// OAuth rate limiting (priority: profile > env vars > defaults)
|
|
813
|
-
rateLimitOAuthMax:
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
...oauthConfig,
|
|
823
|
-
allowed_redirect_hosts: oauthConfig.allowed_redirect_hosts
|
|
824
|
-
|| (process.env.MCP4_ALLOWED_ORIGINS
|
|
825
|
-
? this.extractHostsFromOrigins(process.env.MCP4_ALLOWED_ORIGINS)
|
|
826
|
-
: undefined),
|
|
827
|
-
} : undefined,
|
|
828
|
-
baseUrl, // Pass base URL for token validation
|
|
829
|
-
authConfigs, // Pass auth configs for token validation
|
|
830
|
-
// OAuth resource metadata (priority: profile > OpenAPI > fallback)
|
|
831
|
-
resourceName: this.profile?.resource_name || resourceMetadata.name || 'MCP Server',
|
|
832
|
-
resourceDocumentation: this.profile?.resource_documentation || resourceMetadata.documentation,
|
|
833
|
-
sslCertFile: process.env.MCP4_SSL_CERT_FILE,
|
|
834
|
-
sslKeyFile: process.env.MCP4_SSL_KEY_FILE,
|
|
835
|
-
oauthSessionTimeoutMs: (() => {
|
|
836
|
-
if (process.env.MCP4_OAUTH_SESSION_TIMEOUT_MS === undefined)
|
|
837
|
-
return undefined;
|
|
838
|
-
const parsed = parseInt(process.env.MCP4_OAUTH_SESSION_TIMEOUT_MS, 10);
|
|
839
|
-
if (Number.isNaN(parsed)) {
|
|
840
|
-
throw new ConfigurationError(`Invalid MCP4_OAUTH_SESSION_TIMEOUT_MS: expected integer milliseconds, got '${process.env.MCP4_OAUTH_SESSION_TIMEOUT_MS}'`);
|
|
841
|
-
}
|
|
842
|
-
return parsed;
|
|
843
|
-
})(),
|
|
844
|
-
oauthRefreshThresholdMs: (() => {
|
|
845
|
-
if (process.env.MCP4_OAUTH_REFRESH_THRESHOLD_MS === undefined)
|
|
846
|
-
return undefined;
|
|
847
|
-
const parsed = parseInt(process.env.MCP4_OAUTH_REFRESH_THRESHOLD_MS, 10);
|
|
848
|
-
if (Number.isNaN(parsed)) {
|
|
849
|
-
throw new ConfigurationError(`Invalid MCP4_OAUTH_REFRESH_THRESHOLD_MS: expected integer milliseconds, got '${process.env.MCP4_OAUTH_REFRESH_THRESHOLD_MS}'`);
|
|
850
|
-
}
|
|
851
|
-
return parsed;
|
|
852
|
-
})(),
|
|
971
|
+
rateLimitOAuthMax: profileContext.rateLimitOAuthMax,
|
|
972
|
+
rateLimitOAuthWindowMs: profileContext.rateLimitOAuthWindowMs,
|
|
973
|
+
// OAuth config already merged with allowed_redirect_hosts
|
|
974
|
+
oauthConfig: profileContext.oauthConfig,
|
|
975
|
+
baseUrl: profileContext.baseUrl,
|
|
976
|
+
authConfigs: profileContext.authConfigs,
|
|
977
|
+
resourceName: profileContext.resourceName,
|
|
978
|
+
resourceDocumentation: profileContext.resourceDocumentation,
|
|
979
|
+
parser: profileContext.parser,
|
|
853
980
|
};
|
|
854
981
|
// Warn if binding to non-localhost without explicit MCP4_ALLOWED_ORIGINS
|
|
855
982
|
const isLocalhost = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
@@ -858,37 +985,62 @@ export class MCPServer {
|
|
|
858
985
|
this.logger.warn('Binding to non-localhost with empty MCP4_ALLOWED_ORIGINS. Set MCP4_ALLOWED_ORIGINS or bind to localhost.');
|
|
859
986
|
}
|
|
860
987
|
this.httpTransport = new HttpTransport(config, this.logger);
|
|
988
|
+
const metricsCollector = this.httpTransport.getMetricsCollector?.() || null;
|
|
989
|
+
this.httpClientFactory.setMetricsCollector(metricsCollector);
|
|
990
|
+
this.recordGlobalToolFilterMetrics();
|
|
861
991
|
// Set message handler to process JSON-RPC messages
|
|
862
|
-
this.httpTransport.setMessageHandler(async (message, sessionId) => {
|
|
863
|
-
return await this.handleJsonRpcMessage(message, sessionId);
|
|
992
|
+
this.httpTransport.setMessageHandler(async (message, sessionId, profileId) => {
|
|
993
|
+
return await this.handleJsonRpcMessage(message, sessionId, profileId);
|
|
864
994
|
});
|
|
865
995
|
// Register cleanup listener for session destruction (memory leak prevention)
|
|
866
|
-
this.httpTransport.onSessionDestroyed((sessionId) => {
|
|
867
|
-
this.cleanupSessionClient(sessionId);
|
|
996
|
+
this.httpTransport.onSessionDestroyed((profileId, sessionId) => {
|
|
997
|
+
this.cleanupSessionClient(profileId, sessionId);
|
|
868
998
|
});
|
|
869
999
|
await this.httpTransport.start();
|
|
870
1000
|
this.logger.info('MCP server running on HTTP', { host, port });
|
|
871
1001
|
}
|
|
1002
|
+
attachHttpTransport(transport) {
|
|
1003
|
+
this.httpTransport = transport;
|
|
1004
|
+
const metricsCollector = this.httpTransport.getMetricsCollector?.() || null;
|
|
1005
|
+
this.httpClientFactory.setMetricsCollector(metricsCollector);
|
|
1006
|
+
}
|
|
1007
|
+
handleSessionDestroyed(profileId, sessionId) {
|
|
1008
|
+
this.cleanupSessionClient(profileId, sessionId);
|
|
1009
|
+
}
|
|
872
1010
|
/**
|
|
873
1011
|
* Handle JSON-RPC message from HTTP transport
|
|
874
1012
|
*
|
|
875
1013
|
* Why: Unified message handling for both stdio and HTTP transports
|
|
876
1014
|
*/
|
|
877
|
-
async handleJsonRpcMessage(message, sessionId) {
|
|
1015
|
+
async handleJsonRpcMessage(message, sessionId, profileId) {
|
|
878
1016
|
// Handle initialize
|
|
879
1017
|
if (isInitializeRequest(message)) {
|
|
880
|
-
return this.handleInitialize(message, sessionId);
|
|
1018
|
+
return this.handleInitialize(message, sessionId, profileId);
|
|
881
1019
|
}
|
|
882
1020
|
// Handle tool calls
|
|
883
1021
|
if (isToolCallRequest(message)) {
|
|
884
|
-
return await this.handleToolCall(message, sessionId);
|
|
1022
|
+
return await this.handleToolCall(message, sessionId, profileId);
|
|
885
1023
|
}
|
|
886
1024
|
// Handle other JSON-RPC requests
|
|
887
1025
|
// (tools/list, prompts/list, etc.)
|
|
888
|
-
return await this.handleOtherRequest(message, sessionId);
|
|
1026
|
+
return await this.handleOtherRequest(message, sessionId, profileId);
|
|
1027
|
+
}
|
|
1028
|
+
async handleHttpMessage(message, sessionId, profileId) {
|
|
1029
|
+
return this.handleJsonRpcMessage(message, sessionId, profileId);
|
|
889
1030
|
}
|
|
890
|
-
handleInitialize(message, sessionId) {
|
|
1031
|
+
handleInitialize(message, sessionId, profileId) {
|
|
891
1032
|
const req = message;
|
|
1033
|
+
const params = req.params;
|
|
1034
|
+
if (!this.httpTransport && params?.filtering !== undefined) {
|
|
1035
|
+
if (typeof params.filtering !== 'string') {
|
|
1036
|
+
throw new ValidationError('Invalid X-Mcp4-Params header. Expected comma-separated key=value pairs.');
|
|
1037
|
+
}
|
|
1038
|
+
const parsed = parseFilteringHeader(params.filtering);
|
|
1039
|
+
this.stdioFiltering = parsed.filtering;
|
|
1040
|
+
}
|
|
1041
|
+
if (this.httpTransport && sessionId) {
|
|
1042
|
+
this.applySessionToolFiltering(sessionId, profileId);
|
|
1043
|
+
}
|
|
892
1044
|
const result = {
|
|
893
1045
|
protocolVersion: '2025-03-26',
|
|
894
1046
|
serverInfo: {
|
|
@@ -911,14 +1063,14 @@ export class MCPServer {
|
|
|
911
1063
|
result,
|
|
912
1064
|
};
|
|
913
1065
|
}
|
|
914
|
-
async handleToolCall(message, sessionId) {
|
|
1066
|
+
async handleToolCall(message, sessionId, profileId) {
|
|
915
1067
|
const req = message;
|
|
916
1068
|
const params = req.params;
|
|
917
1069
|
const toolName = params.name;
|
|
918
|
-
const
|
|
1070
|
+
const rawArgs = params.arguments || {};
|
|
919
1071
|
// Check OAuth authentication for tool operations
|
|
920
|
-
if (this.httpTransport && this.httpTransport.hasOAuthProvider()) {
|
|
921
|
-
const authToken = await this.getAuthTokenFromSession(sessionId || '');
|
|
1072
|
+
if (this.httpTransport && this.httpTransport.hasOAuthProvider(profileId)) {
|
|
1073
|
+
const authToken = await this.getAuthTokenFromSession(sessionId || '', profileId);
|
|
922
1074
|
if (!authToken) {
|
|
923
1075
|
// Return OAuth required error with WWW-Authenticate header
|
|
924
1076
|
// This should trigger the OAuth flow in the client
|
|
@@ -930,7 +1082,7 @@ export class MCPServer {
|
|
|
930
1082
|
message: 'Authentication required. Please authorize via OAuth.',
|
|
931
1083
|
data: {
|
|
932
1084
|
oauth_required: true,
|
|
933
|
-
resource_metadata:
|
|
1085
|
+
resource_metadata: this.httpTransport.getOAuthProtectedResourceUrl(profileId),
|
|
934
1086
|
scope: 'api'
|
|
935
1087
|
}
|
|
936
1088
|
}
|
|
@@ -938,16 +1090,36 @@ export class MCPServer {
|
|
|
938
1090
|
return errorResponse;
|
|
939
1091
|
}
|
|
940
1092
|
}
|
|
1093
|
+
let args = rawArgs;
|
|
941
1094
|
try {
|
|
942
1095
|
// Find tool definition
|
|
943
1096
|
const toolDef = this.profile?.tools.find(t => t.name === toolName);
|
|
944
1097
|
if (!toolDef) {
|
|
945
|
-
throw new
|
|
1098
|
+
throw new ResourceNotFoundError(toolName, 'Tool');
|
|
1099
|
+
}
|
|
1100
|
+
args = applyParameterDefaults(toolDef, rawArgs);
|
|
1101
|
+
const toolFilter = this.getToolFilterForSession(sessionId, profileId);
|
|
1102
|
+
if (toolFilter && !toolFilter.allowedToolNames.has(toolName)) {
|
|
1103
|
+
this.recordToolFilterRejection(toolName, 'session');
|
|
1104
|
+
const reason = toolFilter.reasons.get(toolName)?.[0];
|
|
1105
|
+
const reasonSuffix = reason ? ` Blocked by: ${reason}.` : '';
|
|
1106
|
+
throw new AuthorizationError(`Tool '${toolName}' not allowed by X-Mcp4-Tools filter.${reasonSuffix}`);
|
|
1107
|
+
}
|
|
1108
|
+
const filtering = this.getFilteringForSession(sessionId, profileId);
|
|
1109
|
+
if (filtering) {
|
|
1110
|
+
const operation = this.getFilteringOperationInfo(toolDef, args);
|
|
1111
|
+
enforceFiltering({
|
|
1112
|
+
filtering,
|
|
1113
|
+
toolDef,
|
|
1114
|
+
args,
|
|
1115
|
+
parameterAliases: this.profile?.parameter_aliases,
|
|
1116
|
+
operation,
|
|
1117
|
+
});
|
|
946
1118
|
}
|
|
947
1119
|
// Execute tool (reuse existing execution logic)
|
|
948
1120
|
let result;
|
|
949
1121
|
if (toolDef.composite && toolDef.steps) {
|
|
950
|
-
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
1122
|
+
const httpClient = await this.getHttpClientForSession(sessionId, profileId);
|
|
951
1123
|
const compositeResult = await this.compositeExecutor.execute(toolDef.steps, args, toolDef.partial_results || false, httpClient);
|
|
952
1124
|
result = {
|
|
953
1125
|
data: compositeResult.data,
|
|
@@ -958,7 +1130,7 @@ export class MCPServer {
|
|
|
958
1130
|
};
|
|
959
1131
|
}
|
|
960
1132
|
else {
|
|
961
|
-
result = await this.executeSimpleTool(toolDef, args, sessionId);
|
|
1133
|
+
result = await this.executeSimpleTool(toolDef, args, sessionId, profileId);
|
|
962
1134
|
}
|
|
963
1135
|
return {
|
|
964
1136
|
jsonrpc: '2.0',
|
|
@@ -1003,6 +1175,9 @@ export class MCPServer {
|
|
|
1003
1175
|
else if (error instanceof OperationNotFoundError) {
|
|
1004
1176
|
errorCode = -32601; // Method not found
|
|
1005
1177
|
}
|
|
1178
|
+
else if (error instanceof ResourceNotFoundError) {
|
|
1179
|
+
errorCode = -32601; // Method not found
|
|
1180
|
+
}
|
|
1006
1181
|
return {
|
|
1007
1182
|
jsonrpc: '2.0',
|
|
1008
1183
|
id: req.id,
|
|
@@ -1013,11 +1188,35 @@ export class MCPServer {
|
|
|
1013
1188
|
};
|
|
1014
1189
|
}
|
|
1015
1190
|
}
|
|
1016
|
-
|
|
1191
|
+
getFilteringForSession(sessionId, profileId) {
|
|
1192
|
+
if (this.httpTransport && sessionId) {
|
|
1193
|
+
const effectiveProfileId = profileId || this.getProfileIdValue();
|
|
1194
|
+
return this.httpTransport.getSessionFiltering(effectiveProfileId, sessionId);
|
|
1195
|
+
}
|
|
1196
|
+
return this.stdioFiltering;
|
|
1197
|
+
}
|
|
1198
|
+
getToolFilterForSession(sessionId, profileId) {
|
|
1199
|
+
if (this.httpTransport && sessionId && typeof this.httpTransport.getSessionToolFilter === 'function') {
|
|
1200
|
+
const effectiveProfileId = profileId || this.getProfileIdValue();
|
|
1201
|
+
return this.httpTransport.getSessionToolFilter(effectiveProfileId, sessionId);
|
|
1202
|
+
}
|
|
1203
|
+
return undefined;
|
|
1204
|
+
}
|
|
1205
|
+
getFilteringOperationInfo(toolDef, args) {
|
|
1206
|
+
if (toolDef.composite) {
|
|
1207
|
+
return undefined;
|
|
1208
|
+
}
|
|
1209
|
+
const operationId = this.toolGenerator.mapActionToOperation(toolDef, args);
|
|
1210
|
+
if (!operationId) {
|
|
1211
|
+
return undefined;
|
|
1212
|
+
}
|
|
1213
|
+
return this.parser.getOperation(operationId);
|
|
1214
|
+
}
|
|
1215
|
+
async handleOtherRequest(message, sessionId, profileId) {
|
|
1017
1216
|
const req = message;
|
|
1018
1217
|
// Check OAuth authentication for other operations (like tools/list)
|
|
1019
|
-
if (this.httpTransport && this.httpTransport.hasOAuthProvider()) {
|
|
1020
|
-
const authToken = await this.getAuthTokenFromSession(sessionId || '');
|
|
1218
|
+
if (this.httpTransport && this.httpTransport.hasOAuthProvider(profileId)) {
|
|
1219
|
+
const authToken = await this.getAuthTokenFromSession(sessionId || '', profileId);
|
|
1021
1220
|
if (!authToken) {
|
|
1022
1221
|
// Return OAuth required error with WWW-Authenticate header
|
|
1023
1222
|
// This should trigger the OAuth flow in the client
|
|
@@ -1029,7 +1228,7 @@ export class MCPServer {
|
|
|
1029
1228
|
message: 'Authentication required. Please authorize via OAuth.',
|
|
1030
1229
|
data: {
|
|
1031
1230
|
oauth_required: true,
|
|
1032
|
-
resource_metadata:
|
|
1231
|
+
resource_metadata: this.httpTransport.getOAuthProtectedResourceUrl(profileId),
|
|
1033
1232
|
scope: 'api'
|
|
1034
1233
|
}
|
|
1035
1234
|
}
|
|
@@ -1039,7 +1238,11 @@ export class MCPServer {
|
|
|
1039
1238
|
}
|
|
1040
1239
|
// Handle tools/list
|
|
1041
1240
|
if (req.method === 'tools/list') {
|
|
1042
|
-
const
|
|
1241
|
+
const sessionFilter = this.getToolFilterForSession(sessionId, profileId);
|
|
1242
|
+
const allowedSet = sessionFilter?.allowedToolNames;
|
|
1243
|
+
const tools = this.profile?.tools
|
|
1244
|
+
.filter(toolDef => !allowedSet || allowedSet.has(toolDef.name))
|
|
1245
|
+
.map(toolDef => this.toolGenerator.generateTool(toolDef)) || [];
|
|
1043
1246
|
return {
|
|
1044
1247
|
jsonrpc: '2.0',
|
|
1045
1248
|
id: req.id,
|
|
@@ -1058,6 +1261,204 @@ export class MCPServer {
|
|
|
1058
1261
|
},
|
|
1059
1262
|
};
|
|
1060
1263
|
}
|
|
1264
|
+
applyGlobalToolFiltering() {
|
|
1265
|
+
if (!this.profile) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
// Initialize ToolFilterService if not already done
|
|
1269
|
+
if (!this.toolFilterService) {
|
|
1270
|
+
const validator = new RegexValidator();
|
|
1271
|
+
const compiler = new RegexCompiler(validator);
|
|
1272
|
+
const envParser = new EnvConfigParser(compiler);
|
|
1273
|
+
const headerParser = new HeaderConfigParser(compiler);
|
|
1274
|
+
// Create OperationDetector for category filtering
|
|
1275
|
+
const classifier = new OperationClassifier();
|
|
1276
|
+
const resolver = new OpenAPIOperationResolver(this.parser);
|
|
1277
|
+
const detector = new OperationDetector(classifier, resolver);
|
|
1278
|
+
this.toolFilterService = new ToolFilterService(envParser, headerParser, this.logger, detector);
|
|
1279
|
+
}
|
|
1280
|
+
const originalTools = this.profile.tools;
|
|
1281
|
+
const originalCount = originalTools.length;
|
|
1282
|
+
// Apply filtering using new service
|
|
1283
|
+
const filteredTools = this.toolFilterService.applyGlobalFilter(originalTools, process.env);
|
|
1284
|
+
const allowedCount = filteredTools.length;
|
|
1285
|
+
const removedCount = originalCount - allowedCount;
|
|
1286
|
+
// Early return if no filtering config present (service returned same tools)
|
|
1287
|
+
if (filteredTools === originalTools) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
// Validation: check if filter has no effect
|
|
1291
|
+
if (originalCount > 0 && allowedCount === originalCount && removedCount === 0) {
|
|
1292
|
+
throw new ConfigurationError(`Tool filter configuration has no effect. Original tool count: ${originalCount}, filtered: ${allowedCount}. Check MCP4_TOOL_FILTER_* patterns.`);
|
|
1293
|
+
}
|
|
1294
|
+
// Validation: check if all tools filtered
|
|
1295
|
+
if (originalCount > 0 && allowedCount === 0) {
|
|
1296
|
+
throw new ConfigurationError(`All tools filtered out (original: ${originalCount}). Check MCP4_TOOL_FILTER_* settings.`);
|
|
1297
|
+
}
|
|
1298
|
+
// Validate composite tools against filtered operations
|
|
1299
|
+
const resolver = this.buildToolFilterResolver();
|
|
1300
|
+
this.validateCompositeToolsAgainstFilteredOperations(originalTools, filteredTools, resolver);
|
|
1301
|
+
// Update profile
|
|
1302
|
+
this.profile.tools = filteredTools;
|
|
1303
|
+
// Record summary for metrics
|
|
1304
|
+
this.globalToolFilterSummary = {
|
|
1305
|
+
originalCount,
|
|
1306
|
+
allowedCount,
|
|
1307
|
+
removedCount,
|
|
1308
|
+
patternCounts: {
|
|
1309
|
+
// Note: counts not available from new service, using simplified version
|
|
1310
|
+
filtered: removedCount
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
// Warn if high percentage filtered
|
|
1314
|
+
const warnThreshold = this.getToolFilterWarnThresholdPct();
|
|
1315
|
+
if (originalCount > 0) {
|
|
1316
|
+
const percentFiltered = (removedCount / originalCount) * 100;
|
|
1317
|
+
if (percentFiltered >= warnThreshold) {
|
|
1318
|
+
this.logger.warn('Tool filter removed high percentage of tools', {
|
|
1319
|
+
original: originalCount,
|
|
1320
|
+
surviving: allowedCount,
|
|
1321
|
+
threshold_pct: warnThreshold,
|
|
1322
|
+
removed_count: removedCount
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (this.httpTransport) {
|
|
1327
|
+
this.recordGlobalToolFilterMetrics();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
applySessionToolFiltering(sessionId, profileId) {
|
|
1331
|
+
if (!this.httpTransport || !this.profile) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof this.httpTransport.getSessionToolFilterRequest !== 'function') {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const effectiveProfileId = profileId || this.getProfileIdValue();
|
|
1338
|
+
const request = this.httpTransport.getSessionToolFilterRequest(effectiveProfileId, sessionId);
|
|
1339
|
+
if (!request) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const originalCount = this.profile.tools.length;
|
|
1343
|
+
const resolver = this.buildToolFilterResolver();
|
|
1344
|
+
const sessionFilter = applySessionToolFilter(this.profile.tools, request, resolver);
|
|
1345
|
+
const allowedCount = sessionFilter.allowedToolNames.size;
|
|
1346
|
+
if (allowedCount === originalCount) {
|
|
1347
|
+
throw new ValidationError(`X-Mcp4-Tools filter has no effect for this session. Available tools: ${originalCount}, after filter: ${allowedCount}. Check patterns.`);
|
|
1348
|
+
}
|
|
1349
|
+
if (originalCount > 0 && allowedCount === 0) {
|
|
1350
|
+
const sources = request.rawEntries.length > 0 ? request.rawEntries.join(', ') : 'none';
|
|
1351
|
+
throw new ValidationError(`X-Mcp4-Tools filtered out all tools (original: ${originalCount}). Removed by: ${sources}. Check session filter configuration.`);
|
|
1352
|
+
}
|
|
1353
|
+
this.httpTransport.setSessionToolFilter(effectiveProfileId, sessionId, sessionFilter);
|
|
1354
|
+
this.logger.info('Session tool filter applied', {
|
|
1355
|
+
sessionId,
|
|
1356
|
+
originalCount,
|
|
1357
|
+
allowedCount,
|
|
1358
|
+
patterns: request.rawEntries,
|
|
1359
|
+
});
|
|
1360
|
+
this.recordSessionToolFilterMetrics(sessionId, allowedCount, request);
|
|
1361
|
+
}
|
|
1362
|
+
buildToolFilterResolver() {
|
|
1363
|
+
return {
|
|
1364
|
+
getOperationById: (operationId) => this.parser.getOperation(operationId),
|
|
1365
|
+
getOperationForCall: (call) => {
|
|
1366
|
+
const [method, path] = call.split(' ');
|
|
1367
|
+
if (!method || !path) {
|
|
1368
|
+
return undefined;
|
|
1369
|
+
}
|
|
1370
|
+
const pathInfo = this.parser.getPath(path);
|
|
1371
|
+
return pathInfo?.operations[method.toLowerCase()];
|
|
1372
|
+
},
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
validateCompositeToolsAgainstFilteredOperations(originalTools, allowedTools, resolver) {
|
|
1376
|
+
const operationToTools = new Map();
|
|
1377
|
+
for (const tool of originalTools) {
|
|
1378
|
+
if (!tool.operations) {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
for (const operationId of Object.values(tool.operations)) {
|
|
1382
|
+
if (typeof operationId !== 'string') {
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
const names = operationToTools.get(operationId) ?? [];
|
|
1386
|
+
names.push(tool.name);
|
|
1387
|
+
operationToTools.set(operationId, names);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const allowedOperationIds = new Set();
|
|
1391
|
+
for (const tool of allowedTools) {
|
|
1392
|
+
if (!tool.operations) {
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
for (const operationId of Object.values(tool.operations)) {
|
|
1396
|
+
if (typeof operationId !== 'string') {
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
allowedOperationIds.add(operationId);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
for (const tool of allowedTools) {
|
|
1403
|
+
if (!tool.composite || !tool.steps) {
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
for (const step of tool.steps) {
|
|
1407
|
+
const operation = resolver.getOperationForCall(step.call);
|
|
1408
|
+
if (!operation) {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (allowedOperationIds.has(operation.operationId)) {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
const removedTools = operationToTools.get(operation.operationId);
|
|
1415
|
+
if (!removedTools || removedTools.length === 0) {
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
const removedList = removedTools.join(', ');
|
|
1419
|
+
throw new ConfigurationError(`Composite tool '${tool.name}' step '${step.call}' calls filtered tool '${removedList}'. ` +
|
|
1420
|
+
`Add '${removedList}' to filter or include _allow_list or _allow_read if it is a list or read operation.`);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
getToolFilterWarnThresholdPct() {
|
|
1425
|
+
const raw = process.env.MCP4_TOOL_FILTER_WARN_THRESHOLD_PCT;
|
|
1426
|
+
if (raw === undefined) {
|
|
1427
|
+
return 90;
|
|
1428
|
+
}
|
|
1429
|
+
const parsed = Number(raw);
|
|
1430
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
1431
|
+
throw new ConfigurationError(`Invalid MCP4_TOOL_FILTER_WARN_THRESHOLD_PCT: expected positive number, got '${raw}'.`);
|
|
1432
|
+
}
|
|
1433
|
+
return parsed;
|
|
1434
|
+
}
|
|
1435
|
+
recordGlobalToolFilterMetrics() {
|
|
1436
|
+
if (!this.httpTransport || !this.globalToolFilterSummary) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (typeof this.httpTransport.recordGlobalToolFilterMetrics !== 'function') {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
this.httpTransport.recordGlobalToolFilterMetrics(this.globalToolFilterSummary);
|
|
1443
|
+
}
|
|
1444
|
+
recordSessionToolFilterMetrics(sessionId, allowedCount, request) {
|
|
1445
|
+
if (!this.httpTransport) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (typeof this.httpTransport.recordSessionToolFilterMetrics !== 'function') {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
this.httpTransport.recordSessionToolFilterMetrics(sessionId, allowedCount, request);
|
|
1452
|
+
}
|
|
1453
|
+
recordToolFilterRejection(toolName, source) {
|
|
1454
|
+
if (!this.httpTransport) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof this.httpTransport.recordToolFilterRejection !== 'function') {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
this.httpTransport.recordToolFilterRejection(toolName, source);
|
|
1461
|
+
}
|
|
1061
1462
|
/**
|
|
1062
1463
|
* Stop the MCP server gracefully
|
|
1063
1464
|
*
|