mcp4openapi 0.1.0 → 0.2.1

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.
Files changed (152) hide show
  1. package/README.md +137 -95
  2. package/dist/scripts/validate-profile.js +3 -3
  3. package/dist/scripts/validate-profile.js.map +1 -1
  4. package/dist/src/composite-executor.d.ts +3 -1
  5. package/dist/src/composite-executor.d.ts.map +1 -1
  6. package/dist/src/composite-executor.js +16 -5
  7. package/dist/src/composite-executor.js.map +1 -1
  8. package/dist/src/constants.d.ts +49 -0
  9. package/dist/src/constants.d.ts.map +1 -1
  10. package/dist/src/constants.js +49 -0
  11. package/dist/src/constants.js.map +1 -1
  12. package/dist/src/errors.d.ts +6 -0
  13. package/dist/src/errors.d.ts.map +1 -1
  14. package/dist/src/errors.js +13 -0
  15. package/dist/src/errors.js.map +1 -1
  16. package/dist/src/generated-schemas.d.ts +832 -52
  17. package/dist/src/generated-schemas.d.ts.map +1 -1
  18. package/dist/src/generated-schemas.js +31 -8
  19. package/dist/src/generated-schemas.js.map +1 -1
  20. package/dist/src/http-client-factory.d.ts.map +1 -1
  21. package/dist/src/http-client-factory.js +14 -3
  22. package/dist/src/http-client-factory.js.map +1 -1
  23. package/dist/src/http-transport.d.ts +65 -0
  24. package/dist/src/http-transport.d.ts.map +1 -1
  25. package/dist/src/http-transport.js +921 -77
  26. package/dist/src/http-transport.js.map +1 -1
  27. package/dist/src/index.js +108 -8
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/interceptors.d.ts +3 -0
  30. package/dist/src/interceptors.d.ts.map +1 -1
  31. package/dist/src/interceptors.js +76 -8
  32. package/dist/src/interceptors.js.map +1 -1
  33. package/dist/src/logger.d.ts +1 -1
  34. package/dist/src/logger.js +3 -3
  35. package/dist/src/logger.js.map +1 -1
  36. package/dist/src/mcp-server.d.ts +33 -0
  37. package/dist/src/mcp-server.d.ts.map +1 -1
  38. package/dist/src/mcp-server.js +263 -54
  39. package/dist/src/mcp-server.js.map +1 -1
  40. package/dist/src/oauth-provider.d.ts +92 -0
  41. package/dist/src/oauth-provider.d.ts.map +1 -0
  42. package/dist/src/oauth-provider.js +588 -0
  43. package/dist/src/oauth-provider.js.map +1 -0
  44. package/dist/src/openapi-parser.d.ts +16 -0
  45. package/dist/src/openapi-parser.d.ts.map +1 -1
  46. package/dist/src/openapi-parser.js +141 -6
  47. package/dist/src/openapi-parser.js.map +1 -1
  48. package/dist/src/profile-loader.d.ts +2 -2
  49. package/dist/src/profile-loader.d.ts.map +1 -1
  50. package/dist/src/profile-loader.js +45 -24
  51. package/dist/src/profile-loader.js.map +1 -1
  52. package/dist/src/testing/fixtures.d.ts +189 -0
  53. package/dist/src/testing/fixtures.d.ts.map +1 -1
  54. package/dist/src/testing/fixtures.js +144 -0
  55. package/dist/src/testing/fixtures.js.map +1 -1
  56. package/dist/src/testing/mock-gitlab-server.d.ts +26 -17
  57. package/dist/src/testing/mock-gitlab-server.d.ts.map +1 -1
  58. package/dist/src/testing/mock-gitlab-server.js +567 -304
  59. package/dist/src/testing/mock-gitlab-server.js.map +1 -1
  60. package/dist/src/types/http-transport.d.ts +16 -0
  61. package/dist/src/types/http-transport.d.ts.map +1 -1
  62. package/dist/src/types/openapi.d.ts +5 -0
  63. package/dist/src/types/openapi.d.ts.map +1 -1
  64. package/dist/src/types/profile.d.ts +112 -3
  65. package/dist/src/types/profile.d.ts.map +1 -1
  66. package/dist/src/validation-utils.d.ts +12 -0
  67. package/dist/src/validation-utils.d.ts.map +1 -1
  68. package/dist/src/validation-utils.js +17 -0
  69. package/dist/src/validation-utils.js.map +1 -1
  70. package/package.json +12 -3
  71. package/profile-schema.json +169 -7
  72. package/dist/composite-executor.d.ts +0 -65
  73. package/dist/composite-executor.d.ts.map +0 -1
  74. package/dist/composite-executor.js +0 -147
  75. package/dist/composite-executor.js.map +0 -1
  76. package/dist/constants.d.ts +0 -36
  77. package/dist/constants.d.ts.map +0 -1
  78. package/dist/constants.js +0 -36
  79. package/dist/constants.js.map +0 -1
  80. package/dist/http-transport.d.ts +0 -195
  81. package/dist/http-transport.d.ts.map +0 -1
  82. package/dist/http-transport.js +0 -760
  83. package/dist/http-transport.js.map +0 -1
  84. package/dist/interceptors.d.ts +0 -74
  85. package/dist/interceptors.d.ts.map +0 -1
  86. package/dist/interceptors.js +0 -220
  87. package/dist/interceptors.js.map +0 -1
  88. package/dist/logger.d.ts +0 -81
  89. package/dist/logger.d.ts.map +0 -1
  90. package/dist/logger.js +0 -264
  91. package/dist/logger.js.map +0 -1
  92. package/dist/mcp-server.d.ts +0 -110
  93. package/dist/mcp-server.d.ts.map +0 -1
  94. package/dist/mcp-server.js +0 -568
  95. package/dist/mcp-server.js.map +0 -1
  96. package/dist/metrics.d.ts +0 -86
  97. package/dist/metrics.d.ts.map +0 -1
  98. package/dist/metrics.js +0 -229
  99. package/dist/metrics.js.map +0 -1
  100. package/dist/openapi-parser.d.ts +0 -35
  101. package/dist/openapi-parser.d.ts.map +0 -1
  102. package/dist/openapi-parser.js +0 -160
  103. package/dist/openapi-parser.js.map +0 -1
  104. package/dist/profile-loader.d.ts +0 -25
  105. package/dist/profile-loader.d.ts.map +0 -1
  106. package/dist/profile-loader.js +0 -134
  107. package/dist/profile-loader.js.map +0 -1
  108. package/dist/schema-validator.d.ts +0 -32
  109. package/dist/schema-validator.d.ts.map +0 -1
  110. package/dist/schema-validator.js +0 -126
  111. package/dist/schema-validator.js.map +0 -1
  112. package/dist/testing/fixtures.d.ts +0 -186
  113. package/dist/testing/fixtures.d.ts.map +0 -1
  114. package/dist/testing/fixtures.js +0 -135
  115. package/dist/testing/fixtures.js.map +0 -1
  116. package/dist/testing/http-integration.test.d.ts +0 -7
  117. package/dist/testing/http-integration.test.d.ts.map +0 -1
  118. package/dist/testing/http-integration.test.js +0 -383
  119. package/dist/testing/http-integration.test.js.map +0 -1
  120. package/dist/testing/http-multiuser.test.d.ts +0 -10
  121. package/dist/testing/http-multiuser.test.d.ts.map +0 -1
  122. package/dist/testing/http-multiuser.test.js +0 -255
  123. package/dist/testing/http-multiuser.test.js.map +0 -1
  124. package/dist/testing/integration.test.d.ts +0 -8
  125. package/dist/testing/integration.test.d.ts.map +0 -1
  126. package/dist/testing/integration.test.js +0 -247
  127. package/dist/testing/integration.test.js.map +0 -1
  128. package/dist/testing/mock-gitlab-server.d.ts +0 -34
  129. package/dist/testing/mock-gitlab-server.d.ts.map +0 -1
  130. package/dist/testing/mock-gitlab-server.js +0 -224
  131. package/dist/testing/mock-gitlab-server.js.map +0 -1
  132. package/dist/testing/test-types.d.ts +0 -59
  133. package/dist/testing/test-types.d.ts.map +0 -1
  134. package/dist/testing/test-types.js +0 -7
  135. package/dist/testing/test-types.js.map +0 -1
  136. package/dist/tool-generator.d.ts +0 -43
  137. package/dist/tool-generator.d.ts.map +0 -1
  138. package/dist/tool-generator.js +0 -123
  139. package/dist/tool-generator.js.map +0 -1
  140. package/dist/tsconfig.tsbuildinfo +0 -1
  141. package/dist/types/http-transport.d.ts +0 -39
  142. package/dist/types/http-transport.d.ts.map +0 -1
  143. package/dist/types/http-transport.js +0 -8
  144. package/dist/types/http-transport.js.map +0 -1
  145. package/dist/types/openapi.d.ts +0 -50
  146. package/dist/types/openapi.d.ts.map +0 -1
  147. package/dist/types/openapi.js +0 -9
  148. package/dist/types/openapi.js.map +0 -1
  149. package/dist/types/profile.d.ts +0 -76
  150. package/dist/types/profile.d.ts.map +0 -1
  151. package/dist/types/profile.js +0 -9
  152. package/dist/types/profile.js.map +0 -1
@@ -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;AAkBH,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;gBAkBR,MAAM,CAAC,EAAE,MAAM;IAqBrB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDvE;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA8B5B;;OAEG;IACH,OAAO,CAAC,UAAU;IAYlB;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAuC/B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAS/B;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;OAEG;IACH,OAAO,CAAC,aAAa;IA6ErB;;;;;OAKG;YACW,iBAAiB;IAgF/B;;;;;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;IA+CxD;;;;OAIG;YACW,oBAAoB;IAiBlC,OAAO,CAAC,gBAAgB;YA0BV,cAAc;IAiE5B,OAAO,CAAC,kBAAkB;IA6B1B;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAK5B"}
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"}
@@ -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
- if (this.profile.interceptors?.auth) {
89
- this.logger = this.createLoggerWithAuth(this.profile.interceptors.auth);
90
- this.logger.info('Logger re-configured with auth token redaction');
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 authConfig = this.profile.interceptors?.auth;
97
- const hasAuth = !!authConfig;
98
- const envToken = hasAuth ? process.env[authConfig.value_from_env] : undefined;
99
- if (hasAuth && envToken) {
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.LOG_FORMAT || 'console';
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.MCP_TOOLNAME_MAX || '45', 10);
135
- const strategy = (process.env.MCP_TOOLNAME_STRATEGY || 'none').toLowerCase();
136
- const warnOnly = (process.env.MCP_TOOLNAME_WARN_ONLY || 'true').toLowerCase() === 'true';
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.MCP_TOOLNAME_SIMILAR_TOP || '3', 10),
152
- similarityThreshold: parseFloat(process.env.MCP_TOOLNAME_SIMILARITY_THRESHOLD || '0.75'),
153
- minParts: parseInt(process.env.MCP_TOOLNAME_MIN_PARTS || '3', 10),
154
- minLength: parseInt(process.env.MCP_TOOLNAME_MIN_LENGTH || '20', 10),
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 authConfig = this.profile?.interceptors?.auth;
182
- const envVarName = authConfig?.value_from_env || 'API_TOKEN';
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
- this.logger.error('ListTools handler error', err);
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('Internal 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
- this.logger.error('CallTool handler error', err);
288
- // Throw generic error for stdio clients
289
- throw new Error('Internal error');
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 String(args[key]);
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 String(args[alias]);
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.SESSION_TIMEOUT_MS || '1800000', 10),
456
- heartbeatEnabled: process.env.HEARTBEAT_ENABLED === 'true',
457
- heartbeatIntervalMs: parseInt(process.env.HEARTBEAT_INTERVAL_MS || '30000', 10),
458
- metricsEnabled: process.env.METRICS_ENABLED === 'true',
459
- metricsPath: process.env.METRICS_PATH || '/metrics',
460
- allowedOrigins: process.env.ALLOWED_ORIGINS
461
- ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
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.HTTP_RATE_LIMIT_ENABLED !== 'false', // default: true
464
- rateLimitWindowMs: parseInt(process.env.HTTP_RATE_LIMIT_WINDOW_MS || '60000', 10),
465
- rateLimitMaxRequests: parseInt(process.env.HTTP_RATE_LIMIT_MAX_REQUESTS || '100', 10),
466
- rateLimitMetricsMax: parseInt(process.env.HTTP_RATE_LIMIT_METRICS_MAX || '10', 10),
467
- maxTokenLength: process.env.TOKEN_MAX_LENGTH
468
- ? parseInt(process.env.TOKEN_MAX_LENGTH, 10)
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 ALLOWED_ORIGINS
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 ALLOWED_ORIGINS. Set ALLOWED_ORIGINS or bind to localhost.');
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
- // Log internal error details, but return generic message to client
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: -32603,
582
- message: 'Internal error',
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)) || [];