mcp4openapi 0.2.7 → 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 +147 -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 -13
- package/dist/src/core/logger.d.ts.map +1 -0
- package/dist/src/core/logger.js +197 -0
- 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/{naming-warnings.js → core/naming-warnings.js} +6 -6
- 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 +281 -79
- package/dist/src/generated-schemas.d.ts.map +1 -1
- package/dist/src/generated-schemas.js +17 -3
- 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 -156
- 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} +43 -3
- package/dist/src/mcp/mcp-server.d.ts.map +1 -0
- package/dist/src/{mcp-server.js → mcp/mcp-server.js} +639 -123
- 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/listen-support.d.ts +3 -0
- package/dist/src/testing/listen-support.d.ts.map +1 -0
- package/dist/src/testing/listen-support.js +50 -0
- package/dist/src/testing/listen-support.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} +19 -4
- package/dist/src/tooling/proxy-executor.d.ts.map +1 -0
- package/dist/src/tooling/proxy-executor.js +497 -0
- 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/transport/http-transport.js +2522 -0
- 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} +77 -46
- 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 +31 -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 +75 -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 +0 -1826
- 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 +0 -177
- 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 +0 -240
- 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 -138
- 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.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,37 +7,221 @@
|
|
|
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 {
|
|
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';
|
|
23
28
|
export class MCPServer {
|
|
24
29
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Filter response payload to include only specified fields.
|
|
47
|
+
*
|
|
48
|
+
* Supports YouTrack-style field selectors like:
|
|
49
|
+
* - "author(id,login)"
|
|
50
|
+
* - "comments(id,text,author(id,login))"
|
|
51
|
+
*
|
|
52
|
+
* Recurses into nested objects and arrays when subfields are specified.
|
|
27
53
|
*/
|
|
28
54
|
filterFields(data, fields) {
|
|
55
|
+
const selection = this.parseFieldSelection(fields);
|
|
56
|
+
return this.applyFieldSelection(data, selection);
|
|
57
|
+
}
|
|
58
|
+
parseFieldSelection(fields) {
|
|
59
|
+
const root = Object.create(null);
|
|
60
|
+
for (const field of fields) {
|
|
61
|
+
const trimmed = field.trim();
|
|
62
|
+
if (!trimmed)
|
|
63
|
+
continue;
|
|
64
|
+
this.mergeFieldSelector(root, trimmed);
|
|
65
|
+
}
|
|
66
|
+
return root;
|
|
67
|
+
}
|
|
68
|
+
mergeFieldSelector(target, selector) {
|
|
69
|
+
const parsed = this.parseFieldSelector(selector);
|
|
70
|
+
const baseName = parsed.baseName;
|
|
71
|
+
if (!baseName)
|
|
72
|
+
return;
|
|
73
|
+
if (!isSafePropertyName(baseName))
|
|
74
|
+
return;
|
|
75
|
+
if (!parsed.inner) {
|
|
76
|
+
target[baseName] = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const inner = parsed.inner;
|
|
80
|
+
const subSelectors = this.splitTopLevel(inner);
|
|
81
|
+
const subTree = Object.create(null);
|
|
82
|
+
for (const sub of subSelectors) {
|
|
83
|
+
this.mergeFieldSelector(subTree, sub);
|
|
84
|
+
}
|
|
85
|
+
const existing = target[baseName];
|
|
86
|
+
if (existing === true)
|
|
87
|
+
return;
|
|
88
|
+
if (!existing) {
|
|
89
|
+
target[baseName] = subTree;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.mergeSelectionTrees(existing, subTree);
|
|
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
|
+
}
|
|
147
|
+
mergeSelectionTrees(target, incoming) {
|
|
148
|
+
for (const [key, val] of Object.entries(incoming)) {
|
|
149
|
+
if (!isSafePropertyName(key))
|
|
150
|
+
continue;
|
|
151
|
+
const existing = target[key];
|
|
152
|
+
if (!existing) {
|
|
153
|
+
target[key] = val;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (existing === true || val === true) {
|
|
157
|
+
target[key] = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
this.mergeSelectionTrees(existing, val);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
splitTopLevel(input) {
|
|
164
|
+
const result = [];
|
|
165
|
+
let depth = 0;
|
|
166
|
+
let current = '';
|
|
167
|
+
let inQuote = false;
|
|
168
|
+
let escaped = false;
|
|
169
|
+
for (const ch of input) {
|
|
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) {
|
|
192
|
+
const trimmed = current.trim();
|
|
193
|
+
if (trimmed)
|
|
194
|
+
result.push(trimmed);
|
|
195
|
+
current = '';
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
current += ch;
|
|
199
|
+
}
|
|
200
|
+
const last = current.trim();
|
|
201
|
+
if (last)
|
|
202
|
+
result.push(last);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
applyFieldSelection(data, selection) {
|
|
29
206
|
if (!data || typeof data !== 'object') {
|
|
30
207
|
return data;
|
|
31
208
|
}
|
|
32
209
|
if (Array.isArray(data)) {
|
|
33
|
-
return data.map(item => this.
|
|
210
|
+
return data.map(item => this.applyFieldSelection(item, selection));
|
|
34
211
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
212
|
+
const obj = data;
|
|
213
|
+
const filtered = Object.create(null);
|
|
214
|
+
for (const [key, sel] of Object.entries(selection)) {
|
|
215
|
+
if (!isSafePropertyName(key))
|
|
216
|
+
continue;
|
|
217
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key))
|
|
218
|
+
continue;
|
|
219
|
+
const value = obj[key];
|
|
220
|
+
if (sel === true) {
|
|
221
|
+
filtered[key] = value;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
filtered[key] = this.applyFieldSelection(value, sel);
|
|
41
225
|
}
|
|
42
226
|
}
|
|
43
227
|
return filtered;
|
|
@@ -80,6 +264,9 @@ export class MCPServer {
|
|
|
80
264
|
if (error instanceof OperationNotFoundError) {
|
|
81
265
|
return `Operation not found: ${error.message} (correlation ID: ${correlationId})`;
|
|
82
266
|
}
|
|
267
|
+
if (error instanceof ResourceNotFoundError) {
|
|
268
|
+
return `${error.message} (correlation ID: ${correlationId})`;
|
|
269
|
+
}
|
|
83
270
|
// Configuration errors - safe to show (helps admin fix setup)
|
|
84
271
|
if (error instanceof ConfigurationError) {
|
|
85
272
|
return `Configuration error: ${error.message} (correlation ID: ${correlationId})`;
|
|
@@ -126,6 +313,7 @@ export class MCPServer {
|
|
|
126
313
|
// Check if we should warn about long names
|
|
127
314
|
this.checkToolNameLengths();
|
|
128
315
|
}
|
|
316
|
+
this.applyGlobalToolFiltering();
|
|
129
317
|
// Re-create logger with auth config for token redaction
|
|
130
318
|
const authConfigs = this.getAuthConfigs();
|
|
131
319
|
if (authConfigs.length > 0) {
|
|
@@ -142,8 +330,8 @@ export class MCPServer {
|
|
|
142
330
|
const envAuthConfig = this.getEnvBackedAuthConfig();
|
|
143
331
|
const envVarName = envAuthConfig?.value_from_env;
|
|
144
332
|
const envToken = envVarName ? process.env[envVarName] : undefined;
|
|
145
|
-
if (envAuthConfig && envToken) {
|
|
146
|
-
// 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
|
|
147
335
|
const httpClient = this.httpClientFactory.createGlobalClient({
|
|
148
336
|
profile: this.profile,
|
|
149
337
|
baseUrl,
|
|
@@ -249,6 +437,59 @@ export class MCPServer {
|
|
|
249
437
|
const oauthConfig = configs.find(c => c.type === 'oauth');
|
|
250
438
|
return oauthConfig?.oauth_config;
|
|
251
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
|
+
}
|
|
252
493
|
/**
|
|
253
494
|
* Extract hostnames from origin patterns for OAuth redirect validation
|
|
254
495
|
* e.g., "http://localhost:*,https://app.example.com" -> ["localhost", "app.example.com"]
|
|
@@ -288,7 +529,7 @@ export class MCPServer {
|
|
|
288
529
|
/**
|
|
289
530
|
* Get or create HTTP client for session
|
|
290
531
|
*/
|
|
291
|
-
async getHttpClientForSession(sessionId) {
|
|
532
|
+
async getHttpClientForSession(sessionId, profileId) {
|
|
292
533
|
if (!sessionId) {
|
|
293
534
|
// Fallback to global client for stdio transport
|
|
294
535
|
if (!this.httpClientFactory.hasGlobalClient()) {
|
|
@@ -311,7 +552,7 @@ export class MCPServer {
|
|
|
311
552
|
throw new ConfigurationError('Profile not initialized. Call initialize() first.');
|
|
312
553
|
}
|
|
313
554
|
// Get auth token from session (ensures token is valid/refreshed)
|
|
314
|
-
const authToken = await this.getAuthTokenFromSession(sessionId);
|
|
555
|
+
const authToken = await this.getAuthTokenFromSession(sessionId, profileId);
|
|
315
556
|
// Create or get session client using factory
|
|
316
557
|
return this.httpClientFactory.getOrCreateSessionClient(sessionId, {
|
|
317
558
|
profile: this.profile,
|
|
@@ -323,7 +564,7 @@ export class MCPServer {
|
|
|
323
564
|
* Get auth token from HTTP transport session
|
|
324
565
|
* Ensures token is valid (refreshes if expired) before returning
|
|
325
566
|
*/
|
|
326
|
-
async getAuthTokenFromSession(sessionId) {
|
|
567
|
+
async getAuthTokenFromSession(sessionId, profileId) {
|
|
327
568
|
// Early return if sessionId is missing/empty
|
|
328
569
|
// Prevents misleading warn logs with empty sessionId
|
|
329
570
|
if (!sessionId) {
|
|
@@ -333,23 +574,24 @@ export class MCPServer {
|
|
|
333
574
|
return undefined;
|
|
334
575
|
}
|
|
335
576
|
// Ensure token is valid (refresh if expired)
|
|
336
|
-
const
|
|
577
|
+
const effectiveProfileId = profileId || this.getProfileIdValue();
|
|
578
|
+
const isValid = await this.httpTransport.ensureValidSessionToken(effectiveProfileId, sessionId);
|
|
337
579
|
if (!isValid) {
|
|
338
|
-
this.logger.warn('Session token validation/refresh failed', { sessionId });
|
|
580
|
+
this.logger.warn('Session token validation/refresh failed', { profileId: effectiveProfileId, sessionId });
|
|
339
581
|
// Still return token if available - let the API call fail with proper error
|
|
340
582
|
}
|
|
341
583
|
// Use public API instead of type casting
|
|
342
|
-
return this.httpTransport.getSessionToken(sessionId);
|
|
584
|
+
return this.httpTransport.getSessionToken(effectiveProfileId, sessionId);
|
|
343
585
|
}
|
|
344
586
|
/**
|
|
345
587
|
* Cleanup HTTP client for destroyed session
|
|
346
588
|
*
|
|
347
589
|
* Why: Prevent memory leak - sessions expire but cached clients stay forever
|
|
348
590
|
*/
|
|
349
|
-
cleanupSessionClient(sessionId) {
|
|
591
|
+
cleanupSessionClient(profileId, sessionId) {
|
|
350
592
|
const removed = this.httpClientFactory.cleanupSessionClient(sessionId);
|
|
351
593
|
if (removed) {
|
|
352
|
-
this.logger.info('Cleaned up session HTTP client', { sessionId });
|
|
594
|
+
this.logger.info('Cleaned up session HTTP client', { profileId, sessionId });
|
|
353
595
|
}
|
|
354
596
|
}
|
|
355
597
|
/**
|
|
@@ -383,7 +625,8 @@ export class MCPServer {
|
|
|
383
625
|
if (!toolDef) {
|
|
384
626
|
throw new OperationNotFoundError(request.params.name);
|
|
385
627
|
}
|
|
386
|
-
const
|
|
628
|
+
const rawArgs = request.params.arguments || {};
|
|
629
|
+
const args = applyParameterDefaults(toolDef, rawArgs);
|
|
387
630
|
// Validate arguments
|
|
388
631
|
this.toolGenerator.validateArguments(toolDef, args);
|
|
389
632
|
// Execute composite or simple tool
|
|
@@ -433,15 +676,16 @@ export class MCPServer {
|
|
|
433
676
|
* Why separate: Simple tools map directly to single OpenAPI operation.
|
|
434
677
|
* No result aggregation needed.
|
|
435
678
|
*/
|
|
436
|
-
async executeSimpleTool(toolDef, args, sessionId) {
|
|
679
|
+
async executeSimpleTool(toolDef, args, sessionId, profileId) {
|
|
680
|
+
const normalizedArgs = normalizeArguments(toolDef, args);
|
|
437
681
|
this.logger.debug('Executing simple tool', {
|
|
438
682
|
toolName: toolDef.name,
|
|
439
|
-
action:
|
|
440
|
-
resourceType:
|
|
683
|
+
action: normalizedArgs['action'],
|
|
684
|
+
resourceType: normalizedArgs['resource_type'],
|
|
441
685
|
sessionId
|
|
442
686
|
});
|
|
443
687
|
// Get operation definition (can be string or ProxyDownloadOperation)
|
|
444
|
-
const operationDef = this.toolGenerator.getOperationDefinition(toolDef,
|
|
688
|
+
const operationDef = this.toolGenerator.getOperationDefinition(toolDef, normalizedArgs);
|
|
445
689
|
if (!operationDef) {
|
|
446
690
|
throw new ValidationError(`Could not map tool action to operation`, {
|
|
447
691
|
toolName: toolDef.name,
|
|
@@ -452,7 +696,7 @@ export class MCPServer {
|
|
|
452
696
|
}
|
|
453
697
|
// Check if this is a proxy download operation
|
|
454
698
|
if (typeof operationDef === 'object' && operationDef.type === 'proxy_download') {
|
|
455
|
-
return this.executeProxyDownload(operationDef,
|
|
699
|
+
return this.executeProxyDownload(operationDef, normalizedArgs, sessionId, profileId);
|
|
456
700
|
}
|
|
457
701
|
// Regular string operation
|
|
458
702
|
const operationId = operationDef;
|
|
@@ -461,9 +705,9 @@ export class MCPServer {
|
|
|
461
705
|
throw new OperationNotFoundError(operationId);
|
|
462
706
|
}
|
|
463
707
|
// Build request
|
|
464
|
-
const path = this.resolvePath(operation.path,
|
|
465
|
-
const queryParams = this.extractQueryParams(operation,
|
|
466
|
-
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);
|
|
467
711
|
this.logger.debug('Executing HTTP request', {
|
|
468
712
|
operationId,
|
|
469
713
|
method: operation.method,
|
|
@@ -482,9 +726,9 @@ export class MCPServer {
|
|
|
482
726
|
}
|
|
483
727
|
}
|
|
484
728
|
// Execute with session-specific client
|
|
485
|
-
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
729
|
+
const httpClient = await this.getHttpClientForSession(sessionId, profileId);
|
|
486
730
|
// Set fields parameter if response_fields are configured for this action AND enabled
|
|
487
|
-
const action =
|
|
731
|
+
const action = normalizedArgs.action;
|
|
488
732
|
if (toolDef.send_response_fields_as_param && toolDef.response_fields && action && toolDef.response_fields[action]) {
|
|
489
733
|
const fields = toolDef.response_fields[action];
|
|
490
734
|
queryParams.fields = fields.join(',');
|
|
@@ -497,7 +741,7 @@ export class MCPServer {
|
|
|
497
741
|
// Apply response field filtering if configured
|
|
498
742
|
let result = response.body;
|
|
499
743
|
if (toolDef.response_fields) {
|
|
500
|
-
const action =
|
|
744
|
+
const action = normalizedArgs.action;
|
|
501
745
|
if (action && toolDef.response_fields[action]) {
|
|
502
746
|
const fields = toolDef.response_fields[action];
|
|
503
747
|
result = this.filterFields(result, fields);
|
|
@@ -511,7 +755,7 @@ export class MCPServer {
|
|
|
511
755
|
* Why: Some APIs return authenticated URLs that LLMs cannot fetch directly.
|
|
512
756
|
* This proxies the download through the MCP server.
|
|
513
757
|
*/
|
|
514
|
-
async executeProxyDownload(operation, args, sessionId) {
|
|
758
|
+
async executeProxyDownload(operation, args, sessionId, profileId) {
|
|
515
759
|
this.logger.debug('Executing proxy download', {
|
|
516
760
|
metadataEndpoint: operation.metadata_endpoint,
|
|
517
761
|
urlField: operation.url_field,
|
|
@@ -537,10 +781,10 @@ export class MCPServer {
|
|
|
537
781
|
};
|
|
538
782
|
}
|
|
539
783
|
// Get auth credentials for download
|
|
540
|
-
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
784
|
+
const httpClient = await this.getHttpClientForSession(sessionId, profileId);
|
|
541
785
|
const authCredentials = httpClient.getAuthCredentials();
|
|
542
786
|
// Execute proxy download
|
|
543
|
-
const proxyExecutor = new ProxyDownloadExecutor(httpClient);
|
|
787
|
+
const proxyExecutor = new ProxyDownloadExecutor(httpClient, this.logger);
|
|
544
788
|
const result = await proxyExecutor.execute(operation, { path: metadataPath, method: metadataMethod }, authCredentials, directDownloadRequest);
|
|
545
789
|
this.logger.debug('Proxy download completed', {
|
|
546
790
|
fileName: result.fileName,
|
|
@@ -638,17 +882,47 @@ export class MCPServer {
|
|
|
638
882
|
pathOrQuery.add(param.name);
|
|
639
883
|
}
|
|
640
884
|
}
|
|
641
|
-
// 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;
|
|
642
887
|
const bodySchemaProps = new Set();
|
|
643
888
|
if (operation.requestBody?.content) {
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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;
|
|
649
897
|
}
|
|
650
898
|
}
|
|
651
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;
|
|
652
926
|
}
|
|
653
927
|
const body = {};
|
|
654
928
|
let hasBody = false;
|
|
@@ -683,58 +957,26 @@ export class MCPServer {
|
|
|
683
957
|
* and resumability for reliable communication over HTTP.
|
|
684
958
|
*/
|
|
685
959
|
async runHttp(host, port) {
|
|
686
|
-
const { HttpTransport } = await import('
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (oauthConfig) {
|
|
960
|
+
const { HttpTransport } = await import('../transport/http-transport.js');
|
|
961
|
+
const profileContext = this.getHttpProfileContext();
|
|
962
|
+
if (profileContext.oauthConfig) {
|
|
690
963
|
this.logger.info('OAuth authentication enabled for HTTP transport');
|
|
691
964
|
}
|
|
692
|
-
|
|
693
|
-
const authConfigs = this.getAuthConfigs();
|
|
694
|
-
const baseUrl = this.getBaseUrl();
|
|
695
|
-
// Extract OAuth rate limit from profile (if configured)
|
|
696
|
-
const oauthAuthConfig = authConfigs.find(c => c.type === 'oauth');
|
|
697
|
-
const oauthRateLimit = oauthAuthConfig?.oauth_rate_limit;
|
|
698
|
-
// Extract resource metadata from OpenAPI spec or profile
|
|
699
|
-
const resourceMetadata = this.parser.getResourceMetadata();
|
|
965
|
+
const baseConfig = buildHttpTransportBaseConfig(host, port);
|
|
700
966
|
const config = {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
heartbeatEnabled: process.env.MCP4_HEARTBEAT_ENABLED === 'true',
|
|
705
|
-
heartbeatIntervalMs: parseInt(process.env.MCP4_HEARTBEAT_INTERVAL_MS || String(TIMEOUTS.HEARTBEAT_INTERVAL_MS), 10),
|
|
706
|
-
metricsEnabled: process.env.MCP4_METRICS_ENABLED === 'true',
|
|
707
|
-
metricsPath: process.env.MCP4_METRICS_PATH || '/metrics',
|
|
708
|
-
allowedOrigins: process.env.MCP4_ALLOWED_ORIGINS
|
|
709
|
-
? process.env.MCP4_ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
|
710
|
-
: undefined,
|
|
711
|
-
rateLimitEnabled: process.env.MCP4_HTTP_RATE_LIMIT_ENABLED !== 'false', // default: true
|
|
712
|
-
rateLimitWindowMs: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_WINDOW_MS || String(TIMEOUTS.RATE_LIMIT_WINDOW_MS), 10),
|
|
713
|
-
rateLimitMaxRequests: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_MAX_REQUESTS || '100', 10),
|
|
714
|
-
rateLimitMetricsMax: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_METRICS_MAX || '10', 10),
|
|
967
|
+
...baseConfig,
|
|
968
|
+
profileRoutingEnabled: false,
|
|
969
|
+
defaultProfileId: profileContext.profileId,
|
|
715
970
|
// OAuth rate limiting (priority: profile > env vars > defaults)
|
|
716
|
-
rateLimitOAuthMax:
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
...oauthConfig,
|
|
726
|
-
allowed_redirect_hosts: oauthConfig.allowed_redirect_hosts
|
|
727
|
-
|| (process.env.MCP4_ALLOWED_ORIGINS
|
|
728
|
-
? this.extractHostsFromOrigins(process.env.MCP4_ALLOWED_ORIGINS)
|
|
729
|
-
: undefined),
|
|
730
|
-
} : undefined,
|
|
731
|
-
baseUrl, // Pass base URL for token validation
|
|
732
|
-
authConfigs, // Pass auth configs for token validation
|
|
733
|
-
// OAuth resource metadata (priority: profile > OpenAPI > fallback)
|
|
734
|
-
resourceName: this.profile?.resource_name || resourceMetadata.name || 'MCP Server',
|
|
735
|
-
resourceDocumentation: this.profile?.resource_documentation || resourceMetadata.documentation,
|
|
736
|
-
sslCertFile: process.env.MCP4_SSL_CERT_FILE,
|
|
737
|
-
sslKeyFile: process.env.MCP4_SSL_KEY_FILE,
|
|
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,
|
|
738
980
|
};
|
|
739
981
|
// Warn if binding to non-localhost without explicit MCP4_ALLOWED_ORIGINS
|
|
740
982
|
const isLocalhost = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
@@ -743,37 +985,62 @@ export class MCPServer {
|
|
|
743
985
|
this.logger.warn('Binding to non-localhost with empty MCP4_ALLOWED_ORIGINS. Set MCP4_ALLOWED_ORIGINS or bind to localhost.');
|
|
744
986
|
}
|
|
745
987
|
this.httpTransport = new HttpTransport(config, this.logger);
|
|
988
|
+
const metricsCollector = this.httpTransport.getMetricsCollector?.() || null;
|
|
989
|
+
this.httpClientFactory.setMetricsCollector(metricsCollector);
|
|
990
|
+
this.recordGlobalToolFilterMetrics();
|
|
746
991
|
// Set message handler to process JSON-RPC messages
|
|
747
|
-
this.httpTransport.setMessageHandler(async (message, sessionId) => {
|
|
748
|
-
return await this.handleJsonRpcMessage(message, sessionId);
|
|
992
|
+
this.httpTransport.setMessageHandler(async (message, sessionId, profileId) => {
|
|
993
|
+
return await this.handleJsonRpcMessage(message, sessionId, profileId);
|
|
749
994
|
});
|
|
750
995
|
// Register cleanup listener for session destruction (memory leak prevention)
|
|
751
|
-
this.httpTransport.onSessionDestroyed((sessionId) => {
|
|
752
|
-
this.cleanupSessionClient(sessionId);
|
|
996
|
+
this.httpTransport.onSessionDestroyed((profileId, sessionId) => {
|
|
997
|
+
this.cleanupSessionClient(profileId, sessionId);
|
|
753
998
|
});
|
|
754
999
|
await this.httpTransport.start();
|
|
755
1000
|
this.logger.info('MCP server running on HTTP', { host, port });
|
|
756
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
|
+
}
|
|
757
1010
|
/**
|
|
758
1011
|
* Handle JSON-RPC message from HTTP transport
|
|
759
1012
|
*
|
|
760
1013
|
* Why: Unified message handling for both stdio and HTTP transports
|
|
761
1014
|
*/
|
|
762
|
-
async handleJsonRpcMessage(message, sessionId) {
|
|
1015
|
+
async handleJsonRpcMessage(message, sessionId, profileId) {
|
|
763
1016
|
// Handle initialize
|
|
764
1017
|
if (isInitializeRequest(message)) {
|
|
765
|
-
return this.handleInitialize(message, sessionId);
|
|
1018
|
+
return this.handleInitialize(message, sessionId, profileId);
|
|
766
1019
|
}
|
|
767
1020
|
// Handle tool calls
|
|
768
1021
|
if (isToolCallRequest(message)) {
|
|
769
|
-
return await this.handleToolCall(message, sessionId);
|
|
1022
|
+
return await this.handleToolCall(message, sessionId, profileId);
|
|
770
1023
|
}
|
|
771
1024
|
// Handle other JSON-RPC requests
|
|
772
1025
|
// (tools/list, prompts/list, etc.)
|
|
773
|
-
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);
|
|
774
1030
|
}
|
|
775
|
-
handleInitialize(message, sessionId) {
|
|
1031
|
+
handleInitialize(message, sessionId, profileId) {
|
|
776
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
|
+
}
|
|
777
1044
|
const result = {
|
|
778
1045
|
protocolVersion: '2025-03-26',
|
|
779
1046
|
serverInfo: {
|
|
@@ -796,14 +1063,14 @@ export class MCPServer {
|
|
|
796
1063
|
result,
|
|
797
1064
|
};
|
|
798
1065
|
}
|
|
799
|
-
async handleToolCall(message, sessionId) {
|
|
1066
|
+
async handleToolCall(message, sessionId, profileId) {
|
|
800
1067
|
const req = message;
|
|
801
1068
|
const params = req.params;
|
|
802
1069
|
const toolName = params.name;
|
|
803
|
-
const
|
|
1070
|
+
const rawArgs = params.arguments || {};
|
|
804
1071
|
// Check OAuth authentication for tool operations
|
|
805
|
-
if (this.httpTransport && this.httpTransport.hasOAuthProvider()) {
|
|
806
|
-
const authToken = await this.getAuthTokenFromSession(sessionId || '');
|
|
1072
|
+
if (this.httpTransport && this.httpTransport.hasOAuthProvider(profileId)) {
|
|
1073
|
+
const authToken = await this.getAuthTokenFromSession(sessionId || '', profileId);
|
|
807
1074
|
if (!authToken) {
|
|
808
1075
|
// Return OAuth required error with WWW-Authenticate header
|
|
809
1076
|
// This should trigger the OAuth flow in the client
|
|
@@ -815,7 +1082,7 @@ export class MCPServer {
|
|
|
815
1082
|
message: 'Authentication required. Please authorize via OAuth.',
|
|
816
1083
|
data: {
|
|
817
1084
|
oauth_required: true,
|
|
818
|
-
resource_metadata:
|
|
1085
|
+
resource_metadata: this.httpTransport.getOAuthProtectedResourceUrl(profileId),
|
|
819
1086
|
scope: 'api'
|
|
820
1087
|
}
|
|
821
1088
|
}
|
|
@@ -823,16 +1090,36 @@ export class MCPServer {
|
|
|
823
1090
|
return errorResponse;
|
|
824
1091
|
}
|
|
825
1092
|
}
|
|
1093
|
+
let args = rawArgs;
|
|
826
1094
|
try {
|
|
827
1095
|
// Find tool definition
|
|
828
1096
|
const toolDef = this.profile?.tools.find(t => t.name === toolName);
|
|
829
1097
|
if (!toolDef) {
|
|
830
|
-
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
|
+
});
|
|
831
1118
|
}
|
|
832
1119
|
// Execute tool (reuse existing execution logic)
|
|
833
1120
|
let result;
|
|
834
1121
|
if (toolDef.composite && toolDef.steps) {
|
|
835
|
-
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
1122
|
+
const httpClient = await this.getHttpClientForSession(sessionId, profileId);
|
|
836
1123
|
const compositeResult = await this.compositeExecutor.execute(toolDef.steps, args, toolDef.partial_results || false, httpClient);
|
|
837
1124
|
result = {
|
|
838
1125
|
data: compositeResult.data,
|
|
@@ -843,7 +1130,7 @@ export class MCPServer {
|
|
|
843
1130
|
};
|
|
844
1131
|
}
|
|
845
1132
|
else {
|
|
846
|
-
result = await this.executeSimpleTool(toolDef, args, sessionId);
|
|
1133
|
+
result = await this.executeSimpleTool(toolDef, args, sessionId, profileId);
|
|
847
1134
|
}
|
|
848
1135
|
return {
|
|
849
1136
|
jsonrpc: '2.0',
|
|
@@ -888,6 +1175,9 @@ export class MCPServer {
|
|
|
888
1175
|
else if (error instanceof OperationNotFoundError) {
|
|
889
1176
|
errorCode = -32601; // Method not found
|
|
890
1177
|
}
|
|
1178
|
+
else if (error instanceof ResourceNotFoundError) {
|
|
1179
|
+
errorCode = -32601; // Method not found
|
|
1180
|
+
}
|
|
891
1181
|
return {
|
|
892
1182
|
jsonrpc: '2.0',
|
|
893
1183
|
id: req.id,
|
|
@@ -898,11 +1188,35 @@ export class MCPServer {
|
|
|
898
1188
|
};
|
|
899
1189
|
}
|
|
900
1190
|
}
|
|
901
|
-
|
|
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) {
|
|
902
1216
|
const req = message;
|
|
903
1217
|
// Check OAuth authentication for other operations (like tools/list)
|
|
904
|
-
if (this.httpTransport && this.httpTransport.hasOAuthProvider()) {
|
|
905
|
-
const authToken = await this.getAuthTokenFromSession(sessionId || '');
|
|
1218
|
+
if (this.httpTransport && this.httpTransport.hasOAuthProvider(profileId)) {
|
|
1219
|
+
const authToken = await this.getAuthTokenFromSession(sessionId || '', profileId);
|
|
906
1220
|
if (!authToken) {
|
|
907
1221
|
// Return OAuth required error with WWW-Authenticate header
|
|
908
1222
|
// This should trigger the OAuth flow in the client
|
|
@@ -914,7 +1228,7 @@ export class MCPServer {
|
|
|
914
1228
|
message: 'Authentication required. Please authorize via OAuth.',
|
|
915
1229
|
data: {
|
|
916
1230
|
oauth_required: true,
|
|
917
|
-
resource_metadata:
|
|
1231
|
+
resource_metadata: this.httpTransport.getOAuthProtectedResourceUrl(profileId),
|
|
918
1232
|
scope: 'api'
|
|
919
1233
|
}
|
|
920
1234
|
}
|
|
@@ -924,7 +1238,11 @@ export class MCPServer {
|
|
|
924
1238
|
}
|
|
925
1239
|
// Handle tools/list
|
|
926
1240
|
if (req.method === 'tools/list') {
|
|
927
|
-
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)) || [];
|
|
928
1246
|
return {
|
|
929
1247
|
jsonrpc: '2.0',
|
|
930
1248
|
id: req.id,
|
|
@@ -943,6 +1261,204 @@ export class MCPServer {
|
|
|
943
1261
|
},
|
|
944
1262
|
};
|
|
945
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
|
+
}
|
|
946
1462
|
/**
|
|
947
1463
|
* Stop the MCP server gracefully
|
|
948
1464
|
*
|