mcp4openapi 0.1.0 → 0.2.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 +134 -95
- package/dist/scripts/validate-profile.js +3 -3
- package/dist/scripts/validate-profile.js.map +1 -1
- package/dist/src/composite-executor.d.ts +3 -1
- package/dist/src/composite-executor.d.ts.map +1 -1
- package/dist/src/composite-executor.js +16 -5
- package/dist/src/composite-executor.js.map +1 -1
- package/dist/src/constants.d.ts +49 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +49 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +13 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/generated-schemas.d.ts +832 -52
- package/dist/src/generated-schemas.d.ts.map +1 -1
- package/dist/src/generated-schemas.js +31 -8
- package/dist/src/generated-schemas.js.map +1 -1
- package/dist/src/http-client-factory.d.ts.map +1 -1
- package/dist/src/http-client-factory.js +14 -3
- package/dist/src/http-client-factory.js.map +1 -1
- package/dist/src/http-transport.d.ts +65 -0
- package/dist/src/http-transport.d.ts.map +1 -1
- package/dist/src/http-transport.js +921 -77
- package/dist/src/http-transport.js.map +1 -1
- package/dist/src/index.js +108 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/interceptors.d.ts +3 -0
- package/dist/src/interceptors.d.ts.map +1 -1
- package/dist/src/interceptors.js +50 -8
- package/dist/src/interceptors.js.map +1 -1
- package/dist/src/logger.d.ts +1 -1
- package/dist/src/logger.js +3 -3
- package/dist/src/logger.js.map +1 -1
- package/dist/src/mcp-server.d.ts +33 -0
- package/dist/src/mcp-server.d.ts.map +1 -1
- package/dist/src/mcp-server.js +263 -54
- package/dist/src/mcp-server.js.map +1 -1
- package/dist/src/oauth-provider.d.ts +92 -0
- package/dist/src/oauth-provider.d.ts.map +1 -0
- package/dist/src/oauth-provider.js +588 -0
- package/dist/src/oauth-provider.js.map +1 -0
- package/dist/src/openapi-parser.d.ts +16 -0
- package/dist/src/openapi-parser.d.ts.map +1 -1
- package/dist/src/openapi-parser.js +141 -6
- package/dist/src/openapi-parser.js.map +1 -1
- package/dist/src/profile-loader.d.ts +2 -2
- package/dist/src/profile-loader.d.ts.map +1 -1
- package/dist/src/profile-loader.js +45 -24
- package/dist/src/profile-loader.js.map +1 -1
- package/dist/src/testing/fixtures.d.ts +32 -0
- package/dist/src/testing/fixtures.d.ts.map +1 -1
- package/dist/src/testing/fixtures.js +26 -0
- package/dist/src/testing/fixtures.js.map +1 -1
- package/dist/src/testing/mock-gitlab-server.d.ts.map +1 -1
- package/dist/src/testing/mock-gitlab-server.js +131 -1
- package/dist/src/testing/mock-gitlab-server.js.map +1 -1
- package/dist/src/types/http-transport.d.ts +16 -0
- package/dist/src/types/http-transport.d.ts.map +1 -1
- package/dist/src/types/openapi.d.ts +5 -0
- package/dist/src/types/openapi.d.ts.map +1 -1
- package/dist/src/types/profile.d.ts +112 -3
- package/dist/src/types/profile.d.ts.map +1 -1
- package/dist/src/validation-utils.d.ts +12 -0
- package/dist/src/validation-utils.d.ts.map +1 -1
- package/dist/src/validation-utils.js +17 -0
- package/dist/src/validation-utils.js.map +1 -1
- package/package.json +7 -3
- package/profile-schema.json +169 -7
- package/dist/composite-executor.d.ts +0 -65
- package/dist/composite-executor.d.ts.map +0 -1
- package/dist/composite-executor.js +0 -147
- package/dist/composite-executor.js.map +0 -1
- package/dist/constants.d.ts +0 -36
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -36
- package/dist/constants.js.map +0 -1
- package/dist/http-transport.d.ts +0 -195
- package/dist/http-transport.d.ts.map +0 -1
- package/dist/http-transport.js +0 -760
- package/dist/http-transport.js.map +0 -1
- package/dist/interceptors.d.ts +0 -74
- package/dist/interceptors.d.ts.map +0 -1
- package/dist/interceptors.js +0 -220
- package/dist/interceptors.js.map +0 -1
- package/dist/logger.d.ts +0 -81
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -264
- package/dist/logger.js.map +0 -1
- package/dist/mcp-server.d.ts +0 -110
- package/dist/mcp-server.d.ts.map +0 -1
- package/dist/mcp-server.js +0 -568
- package/dist/mcp-server.js.map +0 -1
- package/dist/metrics.d.ts +0 -86
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js +0 -229
- package/dist/metrics.js.map +0 -1
- package/dist/openapi-parser.d.ts +0 -35
- package/dist/openapi-parser.d.ts.map +0 -1
- package/dist/openapi-parser.js +0 -160
- package/dist/openapi-parser.js.map +0 -1
- package/dist/profile-loader.d.ts +0 -25
- package/dist/profile-loader.d.ts.map +0 -1
- package/dist/profile-loader.js +0 -134
- package/dist/profile-loader.js.map +0 -1
- package/dist/schema-validator.d.ts +0 -32
- package/dist/schema-validator.d.ts.map +0 -1
- package/dist/schema-validator.js +0 -126
- package/dist/schema-validator.js.map +0 -1
- package/dist/testing/fixtures.d.ts +0 -186
- package/dist/testing/fixtures.d.ts.map +0 -1
- package/dist/testing/fixtures.js +0 -135
- package/dist/testing/fixtures.js.map +0 -1
- package/dist/testing/http-integration.test.d.ts +0 -7
- package/dist/testing/http-integration.test.d.ts.map +0 -1
- package/dist/testing/http-integration.test.js +0 -383
- package/dist/testing/http-integration.test.js.map +0 -1
- package/dist/testing/http-multiuser.test.d.ts +0 -10
- package/dist/testing/http-multiuser.test.d.ts.map +0 -1
- package/dist/testing/http-multiuser.test.js +0 -255
- package/dist/testing/http-multiuser.test.js.map +0 -1
- package/dist/testing/integration.test.d.ts +0 -8
- package/dist/testing/integration.test.d.ts.map +0 -1
- package/dist/testing/integration.test.js +0 -247
- package/dist/testing/integration.test.js.map +0 -1
- package/dist/testing/mock-gitlab-server.d.ts +0 -34
- package/dist/testing/mock-gitlab-server.d.ts.map +0 -1
- package/dist/testing/mock-gitlab-server.js +0 -224
- package/dist/testing/mock-gitlab-server.js.map +0 -1
- package/dist/testing/test-types.d.ts +0 -59
- package/dist/testing/test-types.d.ts.map +0 -1
- package/dist/testing/test-types.js +0 -7
- package/dist/testing/test-types.js.map +0 -1
- package/dist/tool-generator.d.ts +0 -43
- package/dist/tool-generator.d.ts.map +0 -1
- package/dist/tool-generator.js +0 -123
- package/dist/tool-generator.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/types/http-transport.d.ts +0 -39
- package/dist/types/http-transport.d.ts.map +0 -1
- package/dist/types/http-transport.js +0 -8
- package/dist/types/http-transport.js.map +0 -1
- package/dist/types/openapi.d.ts +0 -50
- package/dist/types/openapi.d.ts.map +0 -1
- package/dist/types/openapi.js +0 -9
- package/dist/types/openapi.js.map +0 -1
- package/dist/types/profile.d.ts +0 -76
- package/dist/types/profile.d.ts.map +0 -1
- package/dist/types/profile.js +0 -9
- package/dist/types/profile.js.map +0 -1
package/dist/src/mcp-server.d.ts
CHANGED
|
@@ -20,6 +20,14 @@ export declare class MCPServer {
|
|
|
20
20
|
* Supports nested objects but keeps first level of arrays
|
|
21
21
|
*/
|
|
22
22
|
private filterFields;
|
|
23
|
+
/**
|
|
24
|
+
* Format error message for client with correlation ID
|
|
25
|
+
*
|
|
26
|
+
* Why: Categorize errors as "safe" (4xx client errors) vs "unsafe" (5xx server errors)
|
|
27
|
+
* Safe errors show API message to help user fix the issue
|
|
28
|
+
* Unsafe errors show generic message to avoid leaking sensitive info
|
|
29
|
+
*/
|
|
30
|
+
private formatErrorForClient;
|
|
23
31
|
constructor(logger?: Logger);
|
|
24
32
|
initialize(specPath: string, profilePath?: string): Promise<void>;
|
|
25
33
|
/**
|
|
@@ -36,12 +44,30 @@ export declare class MCPServer {
|
|
|
36
44
|
* Get base URL from profile config or OpenAPI spec
|
|
37
45
|
*/
|
|
38
46
|
private getBaseUrl;
|
|
47
|
+
/**
|
|
48
|
+
* Get auth configurations as array (supports single or multiple auth methods)
|
|
49
|
+
* Returns array sorted by priority (lower = higher priority)
|
|
50
|
+
*/
|
|
51
|
+
private getAuthConfigs;
|
|
52
|
+
/**
|
|
53
|
+
* Get primary (highest priority) auth configuration
|
|
54
|
+
*/
|
|
55
|
+
private getPrimaryAuthConfig;
|
|
56
|
+
/**
|
|
57
|
+
* Get highest priority auth configuration that reads token from environment
|
|
58
|
+
*/
|
|
59
|
+
private getEnvBackedAuthConfig;
|
|
60
|
+
/**
|
|
61
|
+
* Get OAuth configuration from auth configs (if any)
|
|
62
|
+
*/
|
|
63
|
+
private getOAuthConfig;
|
|
39
64
|
/**
|
|
40
65
|
* Get or create HTTP client for session
|
|
41
66
|
*/
|
|
42
67
|
private getHttpClientForSession;
|
|
43
68
|
/**
|
|
44
69
|
* Get auth token from HTTP transport session
|
|
70
|
+
* Ensures token is valid (refreshes if expired) before returning
|
|
45
71
|
*/
|
|
46
72
|
private getAuthTokenFromSession;
|
|
47
73
|
/**
|
|
@@ -61,6 +87,13 @@ export declare class MCPServer {
|
|
|
61
87
|
* No result aggregation needed.
|
|
62
88
|
*/
|
|
63
89
|
private executeSimpleTool;
|
|
90
|
+
/**
|
|
91
|
+
* Encode path segment if it contains special characters (like slashes)
|
|
92
|
+
*
|
|
93
|
+
* Why: GitLab and other APIs require path parameters (like project paths)
|
|
94
|
+
* to be URL-encoded when used in URL path.
|
|
95
|
+
*/
|
|
96
|
+
private encodePathSegment;
|
|
64
97
|
/**
|
|
65
98
|
* Resolve path parameters using profile aliases
|
|
66
99
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6BH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAO1C,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,iBAAiB,CAAC,CAAoB;IAC9C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,aAAa,CAAa;IAElC;;;OAGG;IACH,OAAO,CAAC,YAAY;IAkBpB;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;gBA8ChB,MAAM,CAAC,EAAE,MAAM;IAqBrB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4DvE;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA8B5B;;OAEG;IACH,OAAO,CAAC,UAAU;IAYlB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAUtB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAK5B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAK9B;;OAEG;IACH,OAAO,CAAC,cAAc;IAMtB;;OAEG;YACW,uBAAuB;IAuCrC;;;OAGG;YACW,uBAAuB;IAsBrC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;OAEG;IACH,OAAO,CAAC,aAAa;IAuFrB;;;;;OAKG;YACW,iBAAiB;IAgF/B;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAKzB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAyBnB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAsB1B;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;IA8BnB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAM/B;;;;;;;OAOG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6ExD;;;;OAIG;YACW,oBAAoB;IAiBlC,OAAO,CAAC,gBAAgB;YA6BV,cAAc;YA8Gd,kBAAkB;IAoDhC;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAK5B"}
|
package/dist/src/mcp-server.js
CHANGED
|
@@ -11,7 +11,8 @@ import { OpenAPIParser } from './openapi-parser.js';
|
|
|
11
11
|
import { ProfileLoader } from './profile-loader.js';
|
|
12
12
|
import { ToolGenerator } from './tool-generator.js';
|
|
13
13
|
import { CompositeExecutor } from './composite-executor.js';
|
|
14
|
-
import { ConfigurationError, OperationNotFoundError, ValidationError } from './errors.js';
|
|
14
|
+
import { ConfigurationError, OperationNotFoundError, ValidationError, AuthenticationError, AuthorizationError, RateLimitError, NetworkError, generateCorrelationId } from './errors.js';
|
|
15
|
+
import { TIMEOUTS, OAUTH_RATE_LIMIT } from './constants.js';
|
|
15
16
|
import { HttpClientFactory } from './http-client-factory.js';
|
|
16
17
|
import { SchemaValidator } from './schema-validator.js';
|
|
17
18
|
import { ConsoleLogger, JsonLogger } from './logger.js';
|
|
@@ -47,6 +48,51 @@ export class MCPServer {
|
|
|
47
48
|
}
|
|
48
49
|
return filtered;
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Format error message for client with correlation ID
|
|
53
|
+
*
|
|
54
|
+
* Why: Categorize errors as "safe" (4xx client errors) vs "unsafe" (5xx server errors)
|
|
55
|
+
* Safe errors show API message to help user fix the issue
|
|
56
|
+
* Unsafe errors show generic message to avoid leaking sensitive info
|
|
57
|
+
*/
|
|
58
|
+
formatErrorForClient(error, correlationId) {
|
|
59
|
+
// Authentication errors - safe to show (token expired, invalid credentials)
|
|
60
|
+
if (error instanceof AuthenticationError) {
|
|
61
|
+
return `Authentication failed: ${error.message} (correlation ID: ${correlationId})`;
|
|
62
|
+
}
|
|
63
|
+
// Authorization errors - safe to show (insufficient permissions)
|
|
64
|
+
if (error instanceof AuthorizationError) {
|
|
65
|
+
return `Authorization failed: ${error.message} (correlation ID: ${correlationId})`;
|
|
66
|
+
}
|
|
67
|
+
// Rate limit errors - safe to show (helps user understand backoff)
|
|
68
|
+
if (error instanceof RateLimitError) {
|
|
69
|
+
const retryInfo = error.details?.retryAfter
|
|
70
|
+
? ` Retry after ${error.details.retryAfter} seconds.`
|
|
71
|
+
: '';
|
|
72
|
+
return `Rate limit exceeded: ${error.message}${retryInfo} (correlation ID: ${correlationId})`;
|
|
73
|
+
}
|
|
74
|
+
// Network errors with 4xx status - safe to show (client errors)
|
|
75
|
+
if (error instanceof NetworkError && error.details?.statusCode) {
|
|
76
|
+
const statusCode = error.details.statusCode;
|
|
77
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
78
|
+
return `Request failed: ${error.message} (correlation ID: ${correlationId})`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Validation errors - safe to show (helps user fix input)
|
|
82
|
+
if (error instanceof ValidationError) {
|
|
83
|
+
return `Validation error: ${error.message} (correlation ID: ${correlationId})`;
|
|
84
|
+
}
|
|
85
|
+
// Operation not found - safe to show (configuration issue)
|
|
86
|
+
if (error instanceof OperationNotFoundError) {
|
|
87
|
+
return `Operation not found: ${error.message} (correlation ID: ${correlationId})`;
|
|
88
|
+
}
|
|
89
|
+
// Configuration errors - safe to show (helps admin fix setup)
|
|
90
|
+
if (error instanceof ConfigurationError) {
|
|
91
|
+
return `Configuration error: ${error.message} (correlation ID: ${correlationId})`;
|
|
92
|
+
}
|
|
93
|
+
// Generic/unknown errors - hide details, show only correlation ID
|
|
94
|
+
return `Internal error (correlation ID: ${correlationId})`;
|
|
95
|
+
}
|
|
50
96
|
constructor(logger) {
|
|
51
97
|
this.logger = logger || new ConsoleLogger();
|
|
52
98
|
this.schemaValidator = new SchemaValidator();
|
|
@@ -66,7 +112,7 @@ export class MCPServer {
|
|
|
66
112
|
// Load OpenAPI spec
|
|
67
113
|
await this.parser.load(specPath);
|
|
68
114
|
this.logger.info('Loaded OpenAPI spec', { specPath });
|
|
69
|
-
// Load profile
|
|
115
|
+
// Load or create MCP profile
|
|
70
116
|
if (profilePath) {
|
|
71
117
|
const loader = new ProfileLoader();
|
|
72
118
|
this.profile = await loader.load(profilePath);
|
|
@@ -85,28 +131,32 @@ export class MCPServer {
|
|
|
85
131
|
this.checkToolNameLengths();
|
|
86
132
|
}
|
|
87
133
|
// Re-create logger with auth config for token redaction
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
134
|
+
const authConfigs = this.getAuthConfigs();
|
|
135
|
+
if (authConfigs.length > 0) {
|
|
136
|
+
// Use first auth config for logger (primary)
|
|
137
|
+
this.logger = this.createLoggerWithAuth(authConfigs[0]);
|
|
138
|
+
this.logger.info('Logger re-configured with auth token redaction', {
|
|
139
|
+
authMethods: authConfigs.length,
|
|
140
|
+
});
|
|
91
141
|
}
|
|
92
142
|
// Setup HTTP client with interceptors
|
|
93
143
|
// For stdio transport, create client with env token
|
|
94
144
|
// For HTTP transport, clients are created per-session with user's token
|
|
95
145
|
const baseUrl = this.getBaseUrl();
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const envToken =
|
|
99
|
-
if (
|
|
146
|
+
const envAuthConfig = this.getEnvBackedAuthConfig();
|
|
147
|
+
const envVarName = envAuthConfig?.value_from_env;
|
|
148
|
+
const envToken = envVarName ? process.env[envVarName] : undefined;
|
|
149
|
+
if (envAuthConfig && envToken) {
|
|
100
150
|
// Token available in env - create global client (stdio transport)
|
|
101
151
|
const httpClient = this.httpClientFactory.createGlobalClient({
|
|
102
152
|
profile: this.profile,
|
|
103
153
|
baseUrl,
|
|
104
154
|
});
|
|
105
|
-
this.compositeExecutor = new CompositeExecutor(this.parser, httpClient);
|
|
155
|
+
this.compositeExecutor = new CompositeExecutor(this.parser, httpClient, this.profile.parameter_aliases);
|
|
106
156
|
}
|
|
107
157
|
else {
|
|
108
158
|
// No env token or no auth - will use per-session clients (HTTP transport)
|
|
109
|
-
this.compositeExecutor = new CompositeExecutor(this.parser);
|
|
159
|
+
this.compositeExecutor = new CompositeExecutor(this.parser, undefined, this.profile.parameter_aliases);
|
|
110
160
|
}
|
|
111
161
|
this.logger.info('MCP server initialized', {
|
|
112
162
|
baseUrl,
|
|
@@ -119,7 +169,7 @@ export class MCPServer {
|
|
|
119
169
|
* Why: Prevents sensitive tokens from appearing in logs
|
|
120
170
|
*/
|
|
121
171
|
createLoggerWithAuth(authConfig) {
|
|
122
|
-
const logFormat = process.env.
|
|
172
|
+
const logFormat = process.env.MCP4_LOG_FORMAT || 'console';
|
|
123
173
|
const logLevel = this.logger instanceof ConsoleLogger || this.logger instanceof JsonLogger
|
|
124
174
|
? this.logger.level
|
|
125
175
|
: undefined;
|
|
@@ -131,9 +181,9 @@ export class MCPServer {
|
|
|
131
181
|
* Check tool name lengths and warn if needed
|
|
132
182
|
*/
|
|
133
183
|
checkToolNameLengths() {
|
|
134
|
-
const maxLength = parseInt(process.env.
|
|
135
|
-
const strategy = (process.env.
|
|
136
|
-
const warnOnly = (process.env.
|
|
184
|
+
const maxLength = parseInt(process.env.MCP4_TOOLNAME_MAX || '45', 10);
|
|
185
|
+
const strategy = (process.env.MCP4_TOOLNAME_STRATEGY || 'none').toLowerCase();
|
|
186
|
+
const warnOnly = (process.env.MCP4_TOOLNAME_WARN_ONLY || 'true').toLowerCase() === 'true';
|
|
137
187
|
// Only warn if strategy is 'none' or warn-only mode is enabled
|
|
138
188
|
if (strategy !== NamingStrategy.None && !warnOnly) {
|
|
139
189
|
return; // Names already shortened, no need to warn
|
|
@@ -148,10 +198,10 @@ export class MCPServer {
|
|
|
148
198
|
}));
|
|
149
199
|
const warningOptions = {
|
|
150
200
|
maxLength,
|
|
151
|
-
similarTopN: parseInt(process.env.
|
|
152
|
-
similarityThreshold: parseFloat(process.env.
|
|
153
|
-
minParts: parseInt(process.env.
|
|
154
|
-
minLength: parseInt(process.env.
|
|
201
|
+
similarTopN: parseInt(process.env.MCP4_TOOLNAME_SIMILAR_TOP || '3', 10),
|
|
202
|
+
similarityThreshold: parseFloat(process.env.MCP4_TOOLNAME_SIMILARITY_THRESHOLD || '0.75'),
|
|
203
|
+
minParts: parseInt(process.env.MCP4_TOOLNAME_MIN_PARTS || '3', 10),
|
|
204
|
+
minLength: parseInt(process.env.MCP4_TOOLNAME_MIN_LENGTH || '20', 10),
|
|
155
205
|
};
|
|
156
206
|
generateNameWarnings(opsForNaming, warningOptions, this.logger);
|
|
157
207
|
}
|
|
@@ -169,17 +219,51 @@ export class MCPServer {
|
|
|
169
219
|
}
|
|
170
220
|
return this.parser.getBaseUrl();
|
|
171
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Get auth configurations as array (supports single or multiple auth methods)
|
|
224
|
+
* Returns array sorted by priority (lower = higher priority)
|
|
225
|
+
*/
|
|
226
|
+
getAuthConfigs() {
|
|
227
|
+
const auth = this.profile?.interceptors?.auth;
|
|
228
|
+
if (!auth)
|
|
229
|
+
return [];
|
|
230
|
+
const configs = Array.isArray(auth) ? auth : [auth];
|
|
231
|
+
// Sort by priority (lower = higher priority)
|
|
232
|
+
return configs.sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get primary (highest priority) auth configuration
|
|
236
|
+
*/
|
|
237
|
+
getPrimaryAuthConfig() {
|
|
238
|
+
const configs = this.getAuthConfigs();
|
|
239
|
+
return configs[0];
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get highest priority auth configuration that reads token from environment
|
|
243
|
+
*/
|
|
244
|
+
getEnvBackedAuthConfig() {
|
|
245
|
+
const configs = this.getAuthConfigs();
|
|
246
|
+
return configs.find(config => config.type !== 'oauth' && !!config.value_from_env);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get OAuth configuration from auth configs (if any)
|
|
250
|
+
*/
|
|
251
|
+
getOAuthConfig() {
|
|
252
|
+
const configs = this.getAuthConfigs();
|
|
253
|
+
const oauthConfig = configs.find(c => c.type === 'oauth');
|
|
254
|
+
return oauthConfig?.oauth_config;
|
|
255
|
+
}
|
|
172
256
|
/**
|
|
173
257
|
* Get or create HTTP client for session
|
|
174
258
|
*/
|
|
175
|
-
getHttpClientForSession(sessionId) {
|
|
259
|
+
async getHttpClientForSession(sessionId) {
|
|
176
260
|
if (!sessionId) {
|
|
177
261
|
// Fallback to global client for stdio transport
|
|
178
262
|
if (!this.httpClientFactory.hasGlobalClient()) {
|
|
179
263
|
const hasHttpTransport = !!this.httpTransport;
|
|
180
264
|
const transport = hasHttpTransport ? 'http' : 'stdio';
|
|
181
|
-
const
|
|
182
|
-
const envVarName =
|
|
265
|
+
const envAuthConfig = this.getEnvBackedAuthConfig();
|
|
266
|
+
const envVarName = envAuthConfig?.value_from_env || 'MCP4_API_TOKEN';
|
|
183
267
|
const hasEnvToken = !!process.env[envVarName];
|
|
184
268
|
throw new ConfigurationError(`HTTP client not initialized. ` +
|
|
185
269
|
`Transport: ${transport}, ` +
|
|
@@ -194,8 +278,8 @@ export class MCPServer {
|
|
|
194
278
|
if (!this.profile) {
|
|
195
279
|
throw new ConfigurationError('Profile not initialized. Call initialize() first.');
|
|
196
280
|
}
|
|
197
|
-
// Get auth token from session
|
|
198
|
-
const authToken = this.getAuthTokenFromSession(sessionId);
|
|
281
|
+
// Get auth token from session (ensures token is valid/refreshed)
|
|
282
|
+
const authToken = await this.getAuthTokenFromSession(sessionId);
|
|
199
283
|
// Create or get session client using factory
|
|
200
284
|
return this.httpClientFactory.getOrCreateSessionClient(sessionId, {
|
|
201
285
|
profile: this.profile,
|
|
@@ -205,11 +289,23 @@ export class MCPServer {
|
|
|
205
289
|
}
|
|
206
290
|
/**
|
|
207
291
|
* Get auth token from HTTP transport session
|
|
292
|
+
* Ensures token is valid (refreshes if expired) before returning
|
|
208
293
|
*/
|
|
209
|
-
getAuthTokenFromSession(sessionId) {
|
|
294
|
+
async getAuthTokenFromSession(sessionId) {
|
|
295
|
+
// Early return if sessionId is missing/empty
|
|
296
|
+
// Prevents misleading warn logs with empty sessionId
|
|
297
|
+
if (!sessionId) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
210
300
|
if (!this.httpTransport) {
|
|
211
301
|
return undefined;
|
|
212
302
|
}
|
|
303
|
+
// Ensure token is valid (refresh if expired)
|
|
304
|
+
const isValid = await this.httpTransport.ensureValidSessionToken(sessionId);
|
|
305
|
+
if (!isValid) {
|
|
306
|
+
this.logger.warn('Session token validation/refresh failed', { sessionId });
|
|
307
|
+
// Still return token if available - let the API call fail with proper error
|
|
308
|
+
}
|
|
213
309
|
// Use public API instead of type casting
|
|
214
310
|
return this.httpTransport.getSessionToken(sessionId);
|
|
215
311
|
}
|
|
@@ -238,9 +334,11 @@ export class MCPServer {
|
|
|
238
334
|
return { tools };
|
|
239
335
|
}
|
|
240
336
|
catch (err) {
|
|
241
|
-
|
|
337
|
+
// Generate correlation ID only on error (lazy)
|
|
338
|
+
const correlationId = generateCorrelationId();
|
|
339
|
+
this.logger.error('ListTools handler error', err, { correlationId });
|
|
242
340
|
// Always return generic error to clients
|
|
243
|
-
throw new Error(
|
|
341
|
+
throw new Error(`Internal error (correlation ID: ${correlationId})`);
|
|
244
342
|
}
|
|
245
343
|
});
|
|
246
344
|
// Execute tool
|
|
@@ -284,9 +382,16 @@ export class MCPServer {
|
|
|
284
382
|
};
|
|
285
383
|
}
|
|
286
384
|
catch (err) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
385
|
+
// Generate correlation ID only on error (lazy)
|
|
386
|
+
const correlationId = generateCorrelationId();
|
|
387
|
+
this.logger.error('CallTool handler error', err, {
|
|
388
|
+
correlationId,
|
|
389
|
+
toolName: request.params.name,
|
|
390
|
+
action: request.params.arguments?.action
|
|
391
|
+
});
|
|
392
|
+
// Return user-friendly error message with correlation ID
|
|
393
|
+
const errorMessage = this.formatErrorForClient(err, correlationId);
|
|
394
|
+
throw new Error(errorMessage);
|
|
290
395
|
}
|
|
291
396
|
});
|
|
292
397
|
}
|
|
@@ -338,7 +443,7 @@ export class MCPServer {
|
|
|
338
443
|
}
|
|
339
444
|
}
|
|
340
445
|
// Execute with session-specific client
|
|
341
|
-
const httpClient = this.getHttpClientForSession(sessionId);
|
|
446
|
+
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
342
447
|
const response = await httpClient.request(operation.method, path, {
|
|
343
448
|
params: queryParams,
|
|
344
449
|
body,
|
|
@@ -355,6 +460,16 @@ export class MCPServer {
|
|
|
355
460
|
}
|
|
356
461
|
return result;
|
|
357
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Encode path segment if it contains special characters (like slashes)
|
|
465
|
+
*
|
|
466
|
+
* Why: GitLab and other APIs require path parameters (like project paths)
|
|
467
|
+
* to be URL-encoded when used in URL path.
|
|
468
|
+
*/
|
|
469
|
+
encodePathSegment(value) {
|
|
470
|
+
const val = String(value);
|
|
471
|
+
return val.includes('/') ? encodeURIComponent(val) : val;
|
|
472
|
+
}
|
|
358
473
|
/**
|
|
359
474
|
* Resolve path parameters using profile aliases
|
|
360
475
|
*
|
|
@@ -366,13 +481,13 @@ export class MCPServer {
|
|
|
366
481
|
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
367
482
|
// Try direct match first
|
|
368
483
|
if (args[key] !== undefined) {
|
|
369
|
-
return
|
|
484
|
+
return this.encodePathSegment(args[key]);
|
|
370
485
|
}
|
|
371
486
|
// Try aliases from profile
|
|
372
487
|
const possibleAliases = aliases[key] || [];
|
|
373
488
|
for (const alias of possibleAliases) {
|
|
374
489
|
if (args[alias] !== undefined) {
|
|
375
|
-
return
|
|
490
|
+
return this.encodePathSegment(args[alias]);
|
|
376
491
|
}
|
|
377
492
|
}
|
|
378
493
|
throw new ValidationError(`Missing path parameter: ${key}` +
|
|
@@ -449,30 +564,56 @@ export class MCPServer {
|
|
|
449
564
|
*/
|
|
450
565
|
async runHttp(host, port) {
|
|
451
566
|
const { HttpTransport } = await import('./http-transport.js');
|
|
567
|
+
// Get OAuth config from profile (supports multi-auth)
|
|
568
|
+
const oauthConfig = this.getOAuthConfig();
|
|
569
|
+
if (oauthConfig) {
|
|
570
|
+
this.logger.info('OAuth authentication enabled for HTTP transport');
|
|
571
|
+
}
|
|
572
|
+
// Get auth configs for token validation
|
|
573
|
+
const authConfigs = this.getAuthConfigs();
|
|
574
|
+
const baseUrl = this.getBaseUrl();
|
|
575
|
+
// Extract OAuth rate limit from profile (if configured)
|
|
576
|
+
const oauthAuthConfig = authConfigs.find(c => c.type === 'oauth');
|
|
577
|
+
const oauthRateLimit = oauthAuthConfig?.oauth_rate_limit;
|
|
578
|
+
// Extract resource metadata from OpenAPI spec or profile
|
|
579
|
+
const resourceMetadata = this.parser.getResourceMetadata();
|
|
452
580
|
const config = {
|
|
453
581
|
host,
|
|
454
582
|
port,
|
|
455
|
-
sessionTimeoutMs: parseInt(process.env.
|
|
456
|
-
heartbeatEnabled: process.env.
|
|
457
|
-
heartbeatIntervalMs: parseInt(process.env.
|
|
458
|
-
metricsEnabled: process.env.
|
|
459
|
-
metricsPath: process.env.
|
|
460
|
-
allowedOrigins: process.env.
|
|
461
|
-
? process.env.
|
|
583
|
+
sessionTimeoutMs: parseInt(process.env.MCP4_SESSION_TIMEOUT_MS || String(TIMEOUTS.SESSION_TIMEOUT_MS), 10),
|
|
584
|
+
heartbeatEnabled: process.env.MCP4_HEARTBEAT_ENABLED === 'true',
|
|
585
|
+
heartbeatIntervalMs: parseInt(process.env.MCP4_HEARTBEAT_INTERVAL_MS || String(TIMEOUTS.HEARTBEAT_INTERVAL_MS), 10),
|
|
586
|
+
metricsEnabled: process.env.MCP4_METRICS_ENABLED === 'true',
|
|
587
|
+
metricsPath: process.env.MCP4_METRICS_PATH || '/metrics',
|
|
588
|
+
allowedOrigins: process.env.MCP4_ALLOWED_ORIGINS
|
|
589
|
+
? process.env.MCP4_ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
|
462
590
|
: undefined,
|
|
463
|
-
rateLimitEnabled: process.env.
|
|
464
|
-
rateLimitWindowMs: parseInt(process.env.
|
|
465
|
-
rateLimitMaxRequests: parseInt(process.env.
|
|
466
|
-
rateLimitMetricsMax: parseInt(process.env.
|
|
467
|
-
|
|
468
|
-
|
|
591
|
+
rateLimitEnabled: process.env.MCP4_HTTP_RATE_LIMIT_ENABLED !== 'false', // default: true
|
|
592
|
+
rateLimitWindowMs: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_WINDOW_MS || String(TIMEOUTS.RATE_LIMIT_WINDOW_MS), 10),
|
|
593
|
+
rateLimitMaxRequests: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_MAX_REQUESTS || '100', 10),
|
|
594
|
+
rateLimitMetricsMax: parseInt(process.env.MCP4_HTTP_RATE_LIMIT_METRICS_MAX || '10', 10),
|
|
595
|
+
// OAuth rate limiting (priority: profile > env vars > defaults)
|
|
596
|
+
rateLimitOAuthMax: oauthRateLimit?.max_requests
|
|
597
|
+
|| parseInt(process.env.MCP4_OAUTH_RATE_LIMIT_MAX || String(OAUTH_RATE_LIMIT.MAX_REQUESTS), 10),
|
|
598
|
+
rateLimitOAuthWindowMs: oauthRateLimit?.window_ms
|
|
599
|
+
|| parseInt(process.env.MCP4_OAUTH_RATE_LIMIT_WINDOW_MS || String(OAUTH_RATE_LIMIT.WINDOW_MS), 10),
|
|
600
|
+
maxTokenLength: process.env.MCP4_TOKEN_MAX_LENGTH
|
|
601
|
+
? parseInt(process.env.MCP4_TOKEN_MAX_LENGTH, 10)
|
|
469
602
|
: undefined, // Uses default from http-transport.ts if undefined
|
|
603
|
+
oauthConfig, // Pass OAuth config if available
|
|
604
|
+
baseUrl, // Pass base URL for token validation
|
|
605
|
+
authConfigs, // Pass auth configs for token validation
|
|
606
|
+
// OAuth resource metadata (priority: profile > OpenAPI > fallback)
|
|
607
|
+
resourceName: this.profile?.resource_name || resourceMetadata.name || 'MCP Server',
|
|
608
|
+
resourceDocumentation: this.profile?.resource_documentation || resourceMetadata.documentation,
|
|
609
|
+
sslCertFile: process.env.MCP4_SSL_CERT_FILE,
|
|
610
|
+
sslKeyFile: process.env.MCP4_SSL_KEY_FILE,
|
|
470
611
|
};
|
|
471
|
-
// Warn if binding to non-localhost without explicit
|
|
612
|
+
// Warn if binding to non-localhost without explicit MCP4_ALLOWED_ORIGINS
|
|
472
613
|
const isLocalhost = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
473
614
|
const hasAllowedOrigins = Array.isArray(config.allowedOrigins) && config.allowedOrigins.length > 0;
|
|
474
615
|
if (!isLocalhost && !hasAllowedOrigins) {
|
|
475
|
-
this.logger.warn('Binding to non-localhost with empty
|
|
616
|
+
this.logger.warn('Binding to non-localhost with empty MCP4_ALLOWED_ORIGINS. Set MCP4_ALLOWED_ORIGINS or bind to localhost.');
|
|
476
617
|
}
|
|
477
618
|
this.httpTransport = new HttpTransport(config, this.logger);
|
|
478
619
|
// Set message handler to process JSON-RPC messages
|
|
@@ -502,7 +643,7 @@ export class MCPServer {
|
|
|
502
643
|
}
|
|
503
644
|
// Handle other JSON-RPC requests
|
|
504
645
|
// (tools/list, prompts/list, etc.)
|
|
505
|
-
return this.handleOtherRequest(message);
|
|
646
|
+
return await this.handleOtherRequest(message, sessionId);
|
|
506
647
|
}
|
|
507
648
|
handleInitialize(message, sessionId) {
|
|
508
649
|
const req = message;
|
|
@@ -516,6 +657,8 @@ export class MCPServer {
|
|
|
516
657
|
tools: {},
|
|
517
658
|
},
|
|
518
659
|
};
|
|
660
|
+
// OAuth capability is communicated via 401 responses with WWW-Authenticate header
|
|
661
|
+
// as per MCP Authorization specification
|
|
519
662
|
// Include sessionId if available (for HTTP transport)
|
|
520
663
|
if (sessionId) {
|
|
521
664
|
result.sessionId = sessionId;
|
|
@@ -531,6 +674,28 @@ export class MCPServer {
|
|
|
531
674
|
const params = req.params;
|
|
532
675
|
const toolName = params.name;
|
|
533
676
|
const args = params.arguments;
|
|
677
|
+
// Check OAuth authentication for tool operations
|
|
678
|
+
if (this.httpTransport && this.httpTransport.hasOAuthProvider()) {
|
|
679
|
+
const authToken = await this.getAuthTokenFromSession(sessionId || '');
|
|
680
|
+
if (!authToken) {
|
|
681
|
+
// Return OAuth required error with WWW-Authenticate header
|
|
682
|
+
// This should trigger the OAuth flow in the client
|
|
683
|
+
const errorResponse = {
|
|
684
|
+
jsonrpc: '2.0',
|
|
685
|
+
id: req.id,
|
|
686
|
+
error: {
|
|
687
|
+
code: -32001, // Application error
|
|
688
|
+
message: 'Authentication required. Please authorize via OAuth.',
|
|
689
|
+
data: {
|
|
690
|
+
oauth_required: true,
|
|
691
|
+
resource_metadata: `${this.httpTransport.getServerUrl()}/.well-known/oauth-protected-resource/mcp`,
|
|
692
|
+
scope: 'api'
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
return errorResponse;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
534
699
|
try {
|
|
535
700
|
// Find tool definition
|
|
536
701
|
const toolDef = this.profile?.tools.find(t => t.name === toolName);
|
|
@@ -540,7 +705,7 @@ export class MCPServer {
|
|
|
540
705
|
// Execute tool (reuse existing execution logic)
|
|
541
706
|
let result;
|
|
542
707
|
if (toolDef.composite && toolDef.steps) {
|
|
543
|
-
const httpClient = this.getHttpClientForSession(sessionId);
|
|
708
|
+
const httpClient = await this.getHttpClientForSession(sessionId);
|
|
544
709
|
const compositeResult = await this.compositeExecutor.execute(toolDef.steps, args, toolDef.partial_results || false, httpClient);
|
|
545
710
|
result = {
|
|
546
711
|
data: compositeResult.data,
|
|
@@ -567,25 +732,69 @@ export class MCPServer {
|
|
|
567
732
|
};
|
|
568
733
|
}
|
|
569
734
|
catch (error) {
|
|
570
|
-
//
|
|
735
|
+
// Generate correlation ID only on error (lazy)
|
|
736
|
+
const correlationId = generateCorrelationId();
|
|
737
|
+
// Log internal error details with correlation ID
|
|
571
738
|
this.logger.error('Tool call error', error, {
|
|
739
|
+
correlationId,
|
|
572
740
|
toolName,
|
|
573
741
|
action: args?.action,
|
|
574
742
|
resourceType: args?.resource_type,
|
|
575
743
|
sessionId
|
|
576
744
|
});
|
|
745
|
+
// Return user-friendly error message with correlation ID
|
|
746
|
+
const errorMessage = this.formatErrorForClient(error, correlationId);
|
|
747
|
+
// Map error type to JSON-RPC error code
|
|
748
|
+
let errorCode = -32603; // Internal error (default)
|
|
749
|
+
if (error instanceof AuthenticationError) {
|
|
750
|
+
errorCode = -32001; // Authentication error
|
|
751
|
+
}
|
|
752
|
+
else if (error instanceof AuthorizationError) {
|
|
753
|
+
errorCode = -32002; // Authorization error
|
|
754
|
+
}
|
|
755
|
+
else if (error instanceof ValidationError) {
|
|
756
|
+
errorCode = -32602; // Invalid params
|
|
757
|
+
}
|
|
758
|
+
else if (error instanceof RateLimitError) {
|
|
759
|
+
errorCode = -32003; // Rate limit error
|
|
760
|
+
}
|
|
761
|
+
else if (error instanceof OperationNotFoundError) {
|
|
762
|
+
errorCode = -32601; // Method not found
|
|
763
|
+
}
|
|
577
764
|
return {
|
|
578
765
|
jsonrpc: '2.0',
|
|
579
766
|
id: req.id,
|
|
580
767
|
error: {
|
|
581
|
-
code:
|
|
582
|
-
message:
|
|
768
|
+
code: errorCode,
|
|
769
|
+
message: errorMessage,
|
|
583
770
|
},
|
|
584
771
|
};
|
|
585
772
|
}
|
|
586
773
|
}
|
|
587
|
-
handleOtherRequest(message) {
|
|
774
|
+
async handleOtherRequest(message, sessionId) {
|
|
588
775
|
const req = message;
|
|
776
|
+
// Check OAuth authentication for other operations (like tools/list)
|
|
777
|
+
if (this.httpTransport && this.httpTransport.hasOAuthProvider()) {
|
|
778
|
+
const authToken = await this.getAuthTokenFromSession(sessionId || '');
|
|
779
|
+
if (!authToken) {
|
|
780
|
+
// Return OAuth required error with WWW-Authenticate header
|
|
781
|
+
// This should trigger the OAuth flow in the client
|
|
782
|
+
const errorResponse = {
|
|
783
|
+
jsonrpc: '2.0',
|
|
784
|
+
id: req.id,
|
|
785
|
+
error: {
|
|
786
|
+
code: -32001, // Application error
|
|
787
|
+
message: 'Authentication required. Please authorize via OAuth.',
|
|
788
|
+
data: {
|
|
789
|
+
oauth_required: true,
|
|
790
|
+
resource_metadata: `${this.httpTransport.getServerUrl()}/.well-known/oauth-protected-resource/mcp`,
|
|
791
|
+
scope: 'api'
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
return errorResponse;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
589
798
|
// Handle tools/list
|
|
590
799
|
if (req.method === 'tools/list') {
|
|
591
800
|
const tools = this.profile?.tools.map(toolDef => this.toolGenerator.generateTool(toolDef)) || [];
|