db-mcp 1.0.2 → 1.1.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/.gitattributes +2 -0
- package/LICENSE +1 -1
- package/README.md +244 -518
- package/dist/chunk-4IA3DB5C.js +135 -0
- package/dist/chunk-AOUL5SHS.js +930 -0
- package/dist/chunk-DWJXQEFA.js +13759 -0
- package/dist/chunk-DZQLDEQS.js +879 -0
- package/dist/chunk-S5IDDSSB.js +74 -0
- package/dist/chunk-Z2GFQU3G.js +363 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +227 -183
- package/dist/http-VSB7DBJR.js +1541 -0
- package/dist/index.d.ts +842 -10
- package/dist/index.js +5 -17
- package/dist/sqlite-VHBA4ABV.js +600 -0
- package/dist/sqlite-native-D2LH5ZT7.js +2498 -0
- package/dist/worker-script.d.ts +2 -0
- package/dist/worker-script.js +126 -0
- package/package.json +26 -16
- package/playwright.config.ts +101 -0
- package/scripts/generate-server-instructions.ts +111 -0
- package/server.json +5 -6
- package/test-server/README.md +118 -0
- package/test-server/code-map.md +409 -0
- package/test-server/fixtures/sample.csv +6 -0
- package/test-server/reset-database.ps1 +373 -0
- package/test-server/sample.csv +11 -0
- package/test-server/test-agent-experience.md +243 -0
- package/test-server/test-database.sql +388 -0
- package/test-server/test-group-tools.md +861 -0
- package/test-server/test-help-resources.mjs +238 -0
- package/test-server/test-preflight.md +53 -0
- package/test-server/test-prompts.md +354 -0
- package/test-server/test-resources.md +245 -0
- package/test-server/test-tool-annotations.mjs +157 -0
- package/test-server/test-tools-advanced-1.md +517 -0
- package/test-server/test-tools-advanced-2.md +487 -0
- package/test-server/test-tools-codemode.md +629 -0
- package/test-server/test-tools.md +176 -0
- package/test-server/tool-reference.md +236 -0
- package/tsconfig.test.json +9 -0
- package/dist/adapters/DatabaseAdapter.d.ts +0 -141
- package/dist/adapters/DatabaseAdapter.d.ts.map +0 -1
- package/dist/adapters/DatabaseAdapter.js +0 -131
- package/dist/adapters/DatabaseAdapter.js.map +0 -1
- package/dist/adapters/sqlite/SchemaManager.d.ts +0 -58
- package/dist/adapters/sqlite/SchemaManager.d.ts.map +0 -1
- package/dist/adapters/sqlite/SchemaManager.js +0 -187
- package/dist/adapters/sqlite/SchemaManager.js.map +0 -1
- package/dist/adapters/sqlite/SqliteAdapter.d.ts +0 -161
- package/dist/adapters/sqlite/SqliteAdapter.d.ts.map +0 -1
- package/dist/adapters/sqlite/SqliteAdapter.js +0 -741
- package/dist/adapters/sqlite/SqliteAdapter.js.map +0 -1
- package/dist/adapters/sqlite/index.d.ts +0 -9
- package/dist/adapters/sqlite/index.d.ts.map +0 -1
- package/dist/adapters/sqlite/index.js +0 -8
- package/dist/adapters/sqlite/index.js.map +0 -1
- package/dist/adapters/sqlite/json-utils.d.ts +0 -100
- package/dist/adapters/sqlite/json-utils.d.ts.map +0 -1
- package/dist/adapters/sqlite/json-utils.js +0 -274
- package/dist/adapters/sqlite/json-utils.js.map +0 -1
- package/dist/adapters/sqlite/output-schemas.d.ts +0 -1187
- package/dist/adapters/sqlite/output-schemas.d.ts.map +0 -1
- package/dist/adapters/sqlite/output-schemas.js +0 -1337
- package/dist/adapters/sqlite/output-schemas.js.map +0 -1
- package/dist/adapters/sqlite/prompts.d.ts +0 -13
- package/dist/adapters/sqlite/prompts.d.ts.map +0 -1
- package/dist/adapters/sqlite/prompts.js +0 -605
- package/dist/adapters/sqlite/prompts.js.map +0 -1
- package/dist/adapters/sqlite/resources.d.ts +0 -13
- package/dist/adapters/sqlite/resources.d.ts.map +0 -1
- package/dist/adapters/sqlite/resources.js +0 -251
- package/dist/adapters/sqlite/resources.js.map +0 -1
- package/dist/adapters/sqlite/tools/admin.d.ts +0 -14
- package/dist/adapters/sqlite/tools/admin.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/admin.js +0 -788
- package/dist/adapters/sqlite/tools/admin.js.map +0 -1
- package/dist/adapters/sqlite/tools/core.d.ts +0 -25
- package/dist/adapters/sqlite/tools/core.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/core.js +0 -359
- package/dist/adapters/sqlite/tools/core.js.map +0 -1
- package/dist/adapters/sqlite/tools/fts.d.ts +0 -13
- package/dist/adapters/sqlite/tools/fts.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/fts.js +0 -347
- package/dist/adapters/sqlite/tools/fts.js.map +0 -1
- package/dist/adapters/sqlite/tools/geo.d.ts +0 -14
- package/dist/adapters/sqlite/tools/geo.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/geo.js +0 -252
- package/dist/adapters/sqlite/tools/geo.js.map +0 -1
- package/dist/adapters/sqlite/tools/index.d.ts +0 -30
- package/dist/adapters/sqlite/tools/index.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/index.js +0 -61
- package/dist/adapters/sqlite/tools/index.js.map +0 -1
- package/dist/adapters/sqlite/tools/json-helpers.d.ts +0 -14
- package/dist/adapters/sqlite/tools/json-helpers.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/json-helpers.js +0 -477
- package/dist/adapters/sqlite/tools/json-helpers.js.map +0 -1
- package/dist/adapters/sqlite/tools/json-operations.d.ts +0 -14
- package/dist/adapters/sqlite/tools/json-operations.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/json-operations.js +0 -839
- package/dist/adapters/sqlite/tools/json-operations.js.map +0 -1
- package/dist/adapters/sqlite/tools/stats.d.ts +0 -15
- package/dist/adapters/sqlite/tools/stats.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/stats.js +0 -1219
- package/dist/adapters/sqlite/tools/stats.js.map +0 -1
- package/dist/adapters/sqlite/tools/text.d.ts +0 -14
- package/dist/adapters/sqlite/tools/text.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/text.js +0 -1141
- package/dist/adapters/sqlite/tools/text.js.map +0 -1
- package/dist/adapters/sqlite/tools/vector.d.ts +0 -14
- package/dist/adapters/sqlite/tools/vector.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/vector.js +0 -613
- package/dist/adapters/sqlite/tools/vector.js.map +0 -1
- package/dist/adapters/sqlite/tools/virtual.d.ts +0 -13
- package/dist/adapters/sqlite/tools/virtual.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/virtual.js +0 -930
- package/dist/adapters/sqlite/tools/virtual.js.map +0 -1
- package/dist/adapters/sqlite/types.d.ts +0 -207
- package/dist/adapters/sqlite/types.d.ts.map +0 -1
- package/dist/adapters/sqlite/types.js +0 -186
- package/dist/adapters/sqlite/types.js.map +0 -1
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts +0 -163
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js +0 -748
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js.map +0 -1
- package/dist/adapters/sqlite-native/index.d.ts +0 -11
- package/dist/adapters/sqlite-native/index.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/index.js +0 -11
- package/dist/adapters/sqlite-native/index.js.map +0 -1
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts +0 -19
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/tools/spatialite.js +0 -628
- package/dist/adapters/sqlite-native/tools/spatialite.js.map +0 -1
- package/dist/adapters/sqlite-native/tools/transactions.d.ts +0 -12
- package/dist/adapters/sqlite-native/tools/transactions.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/tools/transactions.js +0 -255
- package/dist/adapters/sqlite-native/tools/transactions.js.map +0 -1
- package/dist/adapters/sqlite-native/tools/window.d.ts +0 -12
- package/dist/adapters/sqlite-native/tools/window.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/tools/window.js +0 -370
- package/dist/adapters/sqlite-native/tools/window.js.map +0 -1
- package/dist/auth/AuthorizationServerDiscovery.d.ts +0 -90
- package/dist/auth/AuthorizationServerDiscovery.d.ts.map +0 -1
- package/dist/auth/AuthorizationServerDiscovery.js +0 -204
- package/dist/auth/AuthorizationServerDiscovery.js.map +0 -1
- package/dist/auth/OAuthResourceServer.d.ts +0 -65
- package/dist/auth/OAuthResourceServer.d.ts.map +0 -1
- package/dist/auth/OAuthResourceServer.js +0 -121
- package/dist/auth/OAuthResourceServer.js.map +0 -1
- package/dist/auth/TokenValidator.d.ts +0 -60
- package/dist/auth/TokenValidator.d.ts.map +0 -1
- package/dist/auth/TokenValidator.js +0 -235
- package/dist/auth/TokenValidator.js.map +0 -1
- package/dist/auth/errors.d.ts +0 -74
- package/dist/auth/errors.d.ts.map +0 -1
- package/dist/auth/errors.js +0 -133
- package/dist/auth/errors.js.map +0 -1
- package/dist/auth/index.d.ts +0 -13
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -15
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/middleware.d.ts +0 -81
- package/dist/auth/middleware.d.ts.map +0 -1
- package/dist/auth/middleware.js +0 -291
- package/dist/auth/middleware.js.map +0 -1
- package/dist/auth/scopes.d.ts +0 -136
- package/dist/auth/scopes.d.ts.map +0 -1
- package/dist/auth/scopes.js +0 -349
- package/dist/auth/scopes.js.map +0 -1
- package/dist/auth/types.d.ts +0 -257
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js +0 -8
- package/dist/auth/types.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/constants/ServerInstructions.d.ts +0 -45
- package/dist/constants/ServerInstructions.d.ts.map +0 -1
- package/dist/constants/ServerInstructions.js +0 -356
- package/dist/constants/ServerInstructions.js.map +0 -1
- package/dist/filtering/ToolConstants.d.ts +0 -34
- package/dist/filtering/ToolConstants.d.ts.map +0 -1
- package/dist/filtering/ToolConstants.js +0 -174
- package/dist/filtering/ToolConstants.js.map +0 -1
- package/dist/filtering/ToolFilter.d.ts +0 -82
- package/dist/filtering/ToolFilter.d.ts.map +0 -1
- package/dist/filtering/ToolFilter.js +0 -296
- package/dist/filtering/ToolFilter.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server/McpServer.d.ts +0 -61
- package/dist/server/McpServer.d.ts.map +0 -1
- package/dist/server/McpServer.js +0 -270
- package/dist/server/McpServer.js.map +0 -1
- package/dist/transports/http.d.ts +0 -134
- package/dist/transports/http.d.ts.map +0 -1
- package/dist/transports/http.js +0 -516
- package/dist/transports/http.js.map +0 -1
- package/dist/transports/index.d.ts +0 -5
- package/dist/transports/index.d.ts.map +0 -1
- package/dist/transports/index.js +0 -5
- package/dist/transports/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -380
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -68
- package/dist/types/index.js.map +0 -1
- package/dist/utils/annotations.d.ts +0 -44
- package/dist/utils/annotations.d.ts.map +0 -1
- package/dist/utils/annotations.js +0 -77
- package/dist/utils/annotations.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -155
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -329
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/identifiers.d.ts +0 -121
- package/dist/utils/identifiers.d.ts.map +0 -1
- package/dist/utils/identifiers.js +0 -319
- package/dist/utils/identifiers.js.map +0 -1
- package/dist/utils/index.d.ts +0 -7
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -7
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/insightsManager.d.ts +0 -39
- package/dist/utils/insightsManager.d.ts.map +0 -1
- package/dist/utils/insightsManager.js +0 -63
- package/dist/utils/insightsManager.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -189
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -394
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/progress-utils.d.ts +0 -54
- package/dist/utils/progress-utils.d.ts.map +0 -1
- package/dist/utils/progress-utils.js +0 -74
- package/dist/utils/progress-utils.js.map +0 -1
- package/dist/utils/resourceAnnotations.d.ts +0 -36
- package/dist/utils/resourceAnnotations.d.ts.map +0 -1
- package/dist/utils/resourceAnnotations.js +0 -57
- package/dist/utils/resourceAnnotations.js.map +0 -1
- package/dist/utils/where-clause.d.ts +0 -41
- package/dist/utils/where-clause.d.ts.map +0 -1
- package/dist/utils/where-clause.js +0 -116
- package/dist/utils/where-clause.js.map +0 -1
|
@@ -0,0 +1,1541 @@
|
|
|
1
|
+
import './chunk-4IA3DB5C.js';
|
|
2
|
+
import { createModuleLogger, DbMcpError, ERROR_CODES } from './chunk-AOUL5SHS.js';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { localhostHostValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
|
|
5
|
+
import cors from 'cors';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
9
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import * as jose from 'jose';
|
|
11
|
+
|
|
12
|
+
// src/auth/middleware/extraction.ts
|
|
13
|
+
function extractBearerToken(authHeader) {
|
|
14
|
+
if (!authHeader) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const parts = authHeader.split(" ");
|
|
18
|
+
const scheme = parts[0];
|
|
19
|
+
const tokenPart = parts[1];
|
|
20
|
+
if (parts.length !== 2 || scheme?.toLowerCase() !== "bearer") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (tokenPart === void 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const token = tokenPart.trim();
|
|
27
|
+
return token.length > 0 ? token : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/auth/errors.ts
|
|
31
|
+
var OAuthError = class extends DbMcpError {
|
|
32
|
+
/** HTTP status code for this error */
|
|
33
|
+
httpStatus;
|
|
34
|
+
/** WWW-Authenticate header value */
|
|
35
|
+
wwwAuthenticate;
|
|
36
|
+
constructor(message, code, httpStatus, details, wwwAuthenticate) {
|
|
37
|
+
super(message, code, "authentication" /* AUTHENTICATION */, { details });
|
|
38
|
+
this.name = "OAuthError";
|
|
39
|
+
this.httpStatus = httpStatus;
|
|
40
|
+
this.wwwAuthenticate = wwwAuthenticate;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var TokenMissingError = class extends OAuthError {
|
|
44
|
+
constructor(realm = "db-mcp") {
|
|
45
|
+
super(
|
|
46
|
+
"No access token provided",
|
|
47
|
+
ERROR_CODES.AUTH.TOKEN_MISSING.full,
|
|
48
|
+
401,
|
|
49
|
+
void 0,
|
|
50
|
+
`Bearer realm="${realm}"`
|
|
51
|
+
);
|
|
52
|
+
this.name = "TokenMissingError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var InvalidTokenError = class extends OAuthError {
|
|
56
|
+
constructor(message = "Invalid access token", details) {
|
|
57
|
+
super(
|
|
58
|
+
message,
|
|
59
|
+
ERROR_CODES.AUTH.TOKEN_INVALID.full,
|
|
60
|
+
401,
|
|
61
|
+
details,
|
|
62
|
+
'Bearer error="invalid_token"'
|
|
63
|
+
);
|
|
64
|
+
this.name = "InvalidTokenError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var TokenExpiredError = class extends OAuthError {
|
|
68
|
+
constructor(expiredAt) {
|
|
69
|
+
super(
|
|
70
|
+
"Access token has expired",
|
|
71
|
+
ERROR_CODES.AUTH.TOKEN_EXPIRED.full,
|
|
72
|
+
401,
|
|
73
|
+
expiredAt ? { expiredAt: expiredAt.toISOString() } : void 0,
|
|
74
|
+
'Bearer error="invalid_token", error_description="Token has expired"'
|
|
75
|
+
);
|
|
76
|
+
this.name = "TokenExpiredError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var InvalidSignatureError = class extends OAuthError {
|
|
80
|
+
constructor(message = "Token signature verification failed") {
|
|
81
|
+
super(
|
|
82
|
+
message,
|
|
83
|
+
ERROR_CODES.AUTH.SIGNATURE_INVALID.full,
|
|
84
|
+
401,
|
|
85
|
+
void 0,
|
|
86
|
+
'Bearer error="invalid_token", error_description="Signature verification failed"'
|
|
87
|
+
);
|
|
88
|
+
this.name = "InvalidSignatureError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var AuthServerDiscoveryError = class extends OAuthError {
|
|
92
|
+
constructor(serverUrl, cause) {
|
|
93
|
+
super(
|
|
94
|
+
`Failed to discover authorization server metadata: ${serverUrl}`,
|
|
95
|
+
ERROR_CODES.AUTH.DISCOVERY_FAILED.full,
|
|
96
|
+
500,
|
|
97
|
+
{
|
|
98
|
+
serverUrl,
|
|
99
|
+
cause: cause?.message
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
this.name = "AuthServerDiscoveryError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var JwksFetchError = class extends OAuthError {
|
|
106
|
+
constructor(jwksUri, cause) {
|
|
107
|
+
super(
|
|
108
|
+
`Failed to fetch JWKS: ${jwksUri}`,
|
|
109
|
+
ERROR_CODES.AUTH.JWKS_FETCH_FAILED.full,
|
|
110
|
+
500,
|
|
111
|
+
{
|
|
112
|
+
jwksUri,
|
|
113
|
+
cause: cause?.message
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
this.name = "JwksFetchError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function isOAuthError(error) {
|
|
120
|
+
return error instanceof OAuthError;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/auth/scopes/constants.ts
|
|
124
|
+
var SCOPES = {
|
|
125
|
+
/** Read-only access to all databases */
|
|
126
|
+
READ: "read",
|
|
127
|
+
/** Read and write access to all databases */
|
|
128
|
+
WRITE: "write",
|
|
129
|
+
/** Administrative access */
|
|
130
|
+
ADMIN: "admin"};
|
|
131
|
+
var BASE_SCOPES = ["read", "write", "admin", "full"];
|
|
132
|
+
var SCOPE_PATTERNS = {
|
|
133
|
+
/** Database-specific access pattern */
|
|
134
|
+
DATABASE: /^db:([a-zA-Z0-9_-]+)$/,
|
|
135
|
+
/** Table-specific access pattern */
|
|
136
|
+
TABLE: /^table:([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/
|
|
137
|
+
};
|
|
138
|
+
var SUPPORTED_SCOPES = [
|
|
139
|
+
"read",
|
|
140
|
+
"write",
|
|
141
|
+
"admin",
|
|
142
|
+
"full",
|
|
143
|
+
"db:{database}",
|
|
144
|
+
"table:{database}:{table}"
|
|
145
|
+
];
|
|
146
|
+
function parseScopes(scopeString) {
|
|
147
|
+
return scopeString.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/auth/scopes/mapping.ts
|
|
151
|
+
var TOOL_GROUP_SCOPES = {
|
|
152
|
+
core: SCOPES.READ,
|
|
153
|
+
json: SCOPES.READ,
|
|
154
|
+
text: SCOPES.READ,
|
|
155
|
+
stats: SCOPES.READ,
|
|
156
|
+
vector: SCOPES.READ,
|
|
157
|
+
geo: SCOPES.READ,
|
|
158
|
+
introspection: SCOPES.READ,
|
|
159
|
+
migration: SCOPES.WRITE,
|
|
160
|
+
admin: SCOPES.ADMIN,
|
|
161
|
+
codemode: SCOPES.ADMIN
|
|
162
|
+
};
|
|
163
|
+
var groupsForScope = (maxScope) => {
|
|
164
|
+
const hierarchy = {
|
|
165
|
+
read: 0,
|
|
166
|
+
write: 1,
|
|
167
|
+
admin: 2,
|
|
168
|
+
full: 3
|
|
169
|
+
};
|
|
170
|
+
const maxLevel = hierarchy[maxScope];
|
|
171
|
+
return Object.entries(TOOL_GROUP_SCOPES).filter(([, scope]) => hierarchy[scope] <= maxLevel).map(([group]) => group);
|
|
172
|
+
};
|
|
173
|
+
groupsForScope(SCOPES.READ);
|
|
174
|
+
groupsForScope(SCOPES.WRITE);
|
|
175
|
+
groupsForScope(SCOPES.ADMIN);
|
|
176
|
+
|
|
177
|
+
// src/auth/middleware/express-auth.ts
|
|
178
|
+
var logger = createModuleLogger("AUTH");
|
|
179
|
+
function isPublicPath(path, publicPaths) {
|
|
180
|
+
if (path.startsWith("/.well-known/")) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
for (const pattern of publicPaths) {
|
|
184
|
+
if (pattern === path) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (pattern.endsWith("/*")) {
|
|
188
|
+
const prefix = pattern.slice(0, -2);
|
|
189
|
+
if (path === prefix || path.startsWith(prefix + "/")) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
function createAuthMiddleware(config) {
|
|
197
|
+
const { tokenValidator, resourceServer, publicPaths = [] } = config;
|
|
198
|
+
return async (req, res, next) => {
|
|
199
|
+
const requestId = crypto.randomUUID();
|
|
200
|
+
req.requestId = requestId;
|
|
201
|
+
if (isPublicPath(req.path, publicPaths)) {
|
|
202
|
+
logger.info(`Public path accessed: ${req.path}`, {
|
|
203
|
+
code: "AUTH_PUBLIC_PATH",
|
|
204
|
+
requestId,
|
|
205
|
+
path: req.path
|
|
206
|
+
});
|
|
207
|
+
next();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const token = extractBearerToken(req.headers.authorization);
|
|
211
|
+
if (!token) {
|
|
212
|
+
const error = new TokenMissingError(resourceServer.getResourceUri());
|
|
213
|
+
logger.warning("No access token provided", {
|
|
214
|
+
code: ERROR_CODES.AUTH.TOKEN_MISSING.full,
|
|
215
|
+
requestId,
|
|
216
|
+
path: req.path
|
|
217
|
+
});
|
|
218
|
+
res.status(error.httpStatus);
|
|
219
|
+
res.setHeader("WWW-Authenticate", error.wwwAuthenticate ?? "");
|
|
220
|
+
res.json({
|
|
221
|
+
error: "unauthorized",
|
|
222
|
+
error_description: error.message
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const result = await tokenValidator.validate(token);
|
|
227
|
+
if (!result.valid) {
|
|
228
|
+
logger.warning(
|
|
229
|
+
`Token validation failed: ${result.error ?? "Unknown error"}`,
|
|
230
|
+
{
|
|
231
|
+
code: result.errorCode ?? ERROR_CODES.AUTH.TOKEN_INVALID.full,
|
|
232
|
+
requestId,
|
|
233
|
+
path: req.path
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
res.status(401);
|
|
237
|
+
res.setHeader(
|
|
238
|
+
"WWW-Authenticate",
|
|
239
|
+
resourceServer.getWWWAuthenticateHeader("invalid_token", result.error)
|
|
240
|
+
);
|
|
241
|
+
res.json({
|
|
242
|
+
error: "invalid_token",
|
|
243
|
+
error_description: result.error
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const claims = result.claims;
|
|
248
|
+
if (!claims) {
|
|
249
|
+
res.status(500).json({ error: "internal_error" });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
req.auth = claims;
|
|
253
|
+
req.accessToken = token;
|
|
254
|
+
logger.info(`Request authenticated: ${claims.sub}`, {
|
|
255
|
+
code: "AUTH_SUCCESS",
|
|
256
|
+
requestId,
|
|
257
|
+
sub: claims.sub,
|
|
258
|
+
scopes: claims.scopes.length,
|
|
259
|
+
path: req.path
|
|
260
|
+
});
|
|
261
|
+
next();
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/auth/middleware/express-scopes.ts
|
|
266
|
+
createModuleLogger("AUTH");
|
|
267
|
+
function oauthErrorHandler(error, _req, res, next) {
|
|
268
|
+
if (isOAuthError(error)) {
|
|
269
|
+
res.status(error.httpStatus);
|
|
270
|
+
if (error.wwwAuthenticate) {
|
|
271
|
+
res.setHeader("WWW-Authenticate", error.wwwAuthenticate);
|
|
272
|
+
}
|
|
273
|
+
res.json({
|
|
274
|
+
error: error.code,
|
|
275
|
+
error_description: error.message
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
next(error);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/transports/http/types.ts
|
|
283
|
+
var DEFAULT_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
284
|
+
var DEFAULT_RATE_LIMIT_MAX = 100;
|
|
285
|
+
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
286
|
+
var DEFAULT_HSTS_MAX_AGE = 31536e3;
|
|
287
|
+
var JSONRPC_SERVER_ERROR = -32e3;
|
|
288
|
+
var JSONRPC_INTERNAL_ERROR = -32603;
|
|
289
|
+
var HTTP_REQUEST_TIMEOUT_MS = 12e4;
|
|
290
|
+
var HTTP_KEEP_ALIVE_TIMEOUT_MS = 65e3;
|
|
291
|
+
var HTTP_HEADERS_TIMEOUT_MS = 66e3;
|
|
292
|
+
function getClientIp(req, trustProxy) {
|
|
293
|
+
if (trustProxy) {
|
|
294
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
295
|
+
if (typeof forwarded === "string") {
|
|
296
|
+
const firstIp = forwarded.split(",")[0]?.trim();
|
|
297
|
+
if (firstIp) return firstIp;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
301
|
+
}
|
|
302
|
+
function setupSecurityHeaders(state) {
|
|
303
|
+
if (!state.app) return;
|
|
304
|
+
state.app.use((_req, res, next) => {
|
|
305
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
306
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
307
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
308
|
+
res.setHeader(
|
|
309
|
+
"Content-Security-Policy",
|
|
310
|
+
"default-src 'none'; frame-ancestors 'none'"
|
|
311
|
+
);
|
|
312
|
+
res.setHeader(
|
|
313
|
+
"Permissions-Policy",
|
|
314
|
+
"camera=(), microphone=(), geolocation=()"
|
|
315
|
+
);
|
|
316
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
317
|
+
if (state.config.enableHSTS) {
|
|
318
|
+
const maxAge = state.config.hstsMaxAge ?? DEFAULT_HSTS_MAX_AGE;
|
|
319
|
+
res.setHeader(
|
|
320
|
+
"Strict-Transport-Security",
|
|
321
|
+
`max-age=${String(maxAge)}; includeSubDomains`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
next();
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function matchesCorsOrigin(origin, pattern) {
|
|
328
|
+
if (pattern === "*") return true;
|
|
329
|
+
if (pattern.startsWith("*.")) {
|
|
330
|
+
const domain = pattern.slice(1);
|
|
331
|
+
return origin.endsWith(domain) && origin.length > domain.length;
|
|
332
|
+
}
|
|
333
|
+
return origin === pattern;
|
|
334
|
+
}
|
|
335
|
+
function setupCors(state) {
|
|
336
|
+
if (!state.app) return;
|
|
337
|
+
const corsOrigins = state.config.corsOrigins ?? ["*"];
|
|
338
|
+
const isWildcard = corsOrigins.includes("*");
|
|
339
|
+
state.app.use((req, res, next) => {
|
|
340
|
+
const origin = req.headers.origin;
|
|
341
|
+
if (isWildcard) {
|
|
342
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
343
|
+
} else if (origin && corsOrigins.some((pattern) => matchesCorsOrigin(origin, pattern))) {
|
|
344
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
345
|
+
res.setHeader("Vary", "Origin");
|
|
346
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
347
|
+
}
|
|
348
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
349
|
+
res.setHeader(
|
|
350
|
+
"Access-Control-Allow-Headers",
|
|
351
|
+
"Content-Type, Accept, Authorization, mcp-session-id, Last-Event-ID, mcp-protocol-version"
|
|
352
|
+
);
|
|
353
|
+
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
354
|
+
if (req.method === "OPTIONS") {
|
|
355
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
356
|
+
res.status(204).end();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
next();
|
|
360
|
+
});
|
|
361
|
+
if (state.config.cors) {
|
|
362
|
+
state.app.use(cors(state.config.cors));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function setupRateLimiting(state) {
|
|
366
|
+
if (!state.app) return;
|
|
367
|
+
const windowMs = DEFAULT_RATE_LIMIT_WINDOW_MS;
|
|
368
|
+
const maxRequests = process.env["MCP_RATE_LIMIT_MAX"] ? parseInt(process.env["MCP_RATE_LIMIT_MAX"], 10) : DEFAULT_RATE_LIMIT_MAX;
|
|
369
|
+
const trustProxy = state.config.trustProxy ?? false;
|
|
370
|
+
state.rateLimitCleanupTimer = setInterval(() => {
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
for (const [ip, entry] of state.rateLimitMap) {
|
|
373
|
+
if (now >= entry.resetAt) {
|
|
374
|
+
state.rateLimitMap.delete(ip);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}, windowMs);
|
|
378
|
+
state.rateLimitCleanupTimer.unref();
|
|
379
|
+
state.app.use((req, res, next) => {
|
|
380
|
+
if (req.path === "/health") {
|
|
381
|
+
next();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const ip = getClientIp(req, trustProxy);
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
let entry = state.rateLimitMap.get(ip);
|
|
387
|
+
if (!entry || now >= entry.resetAt) {
|
|
388
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
389
|
+
state.rateLimitMap.set(ip, entry);
|
|
390
|
+
}
|
|
391
|
+
entry.count++;
|
|
392
|
+
if (entry.count > maxRequests) {
|
|
393
|
+
const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
|
|
394
|
+
res.setHeader("Retry-After", String(retryAfter));
|
|
395
|
+
res.status(429).json({
|
|
396
|
+
error: "Too Many Requests",
|
|
397
|
+
retryAfter
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
next();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/transports/http/type-adapters.ts
|
|
406
|
+
function asIncoming(req) {
|
|
407
|
+
return req;
|
|
408
|
+
}
|
|
409
|
+
function asServerResponse(res) {
|
|
410
|
+
return res;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/transports/http/session.ts
|
|
414
|
+
var logger3 = createModuleLogger("HTTP");
|
|
415
|
+
async function setupStatelessEndpoints(state) {
|
|
416
|
+
if (!state.app || !state.mcpServer) {
|
|
417
|
+
throw new DbMcpError(
|
|
418
|
+
"Transport or server not initialized",
|
|
419
|
+
ERROR_CODES.SERVER.TRANSPORT_ERROR.full,
|
|
420
|
+
"internal" /* INTERNAL */
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
state.statelessTransport = new StreamableHTTPServerTransport({
|
|
424
|
+
enableJsonResponse: true
|
|
425
|
+
});
|
|
426
|
+
state.statelessTransport.onclose ??= () => {
|
|
427
|
+
logger3.debug("Stateless transport closed", {
|
|
428
|
+
code: "HTTP_STATELESS_CLOSE"
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
await state.mcpServer.connect(
|
|
432
|
+
state.statelessTransport
|
|
433
|
+
);
|
|
434
|
+
logger3.info("Stateless transport connected", { code: "HTTP_STATELESS" });
|
|
435
|
+
state.app.post("/mcp", (req, res) => {
|
|
436
|
+
if (!state.statelessTransport) {
|
|
437
|
+
res.status(500).json({ error: "Transport not initialized" });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
void state.statelessTransport.handleRequest(
|
|
441
|
+
asIncoming(req),
|
|
442
|
+
asServerResponse(res),
|
|
443
|
+
req.body
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
state.app.get("/mcp", (_req, res) => {
|
|
447
|
+
res.status(405).json({
|
|
448
|
+
jsonrpc: "2.0",
|
|
449
|
+
error: {
|
|
450
|
+
code: JSONRPC_SERVER_ERROR,
|
|
451
|
+
message: "SSE streaming not available in stateless mode"
|
|
452
|
+
},
|
|
453
|
+
id: null
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
state.app.delete("/mcp", (_req, res) => {
|
|
457
|
+
res.status(204).end();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function setupStatefulEndpoints(state) {
|
|
461
|
+
if (!state.app || !state.mcpServer) {
|
|
462
|
+
throw new DbMcpError(
|
|
463
|
+
"Transport or server not initialized",
|
|
464
|
+
ERROR_CODES.SERVER.TRANSPORT_ERROR.full,
|
|
465
|
+
"internal" /* INTERNAL */
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const server = state.mcpServer;
|
|
469
|
+
state.app.post("/mcp", (req, res) => {
|
|
470
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
471
|
+
if (sessionId && state.sseTransports.has(sessionId)) {
|
|
472
|
+
res.status(400).json({
|
|
473
|
+
jsonrpc: "2.0",
|
|
474
|
+
error: {
|
|
475
|
+
code: JSONRPC_SERVER_ERROR,
|
|
476
|
+
message: "Bad Request: Session exists but uses a different transport protocol"
|
|
477
|
+
},
|
|
478
|
+
id: null
|
|
479
|
+
});
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const isNewSessionRequest = (body) => {
|
|
483
|
+
if (isInitializeRequest(body)) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if (Array.isArray(body) && body.length > 0) {
|
|
487
|
+
return isInitializeRequest(body[0]);
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
};
|
|
491
|
+
void (async () => {
|
|
492
|
+
try {
|
|
493
|
+
let httpTransport;
|
|
494
|
+
if (sessionId && state.transports.has(sessionId)) {
|
|
495
|
+
httpTransport = state.transports.get(sessionId);
|
|
496
|
+
} else if (sessionId === void 0 && isNewSessionRequest(req.body)) {
|
|
497
|
+
const newTransport = new StreamableHTTPServerTransport({
|
|
498
|
+
sessionIdGenerator: () => randomUUID(),
|
|
499
|
+
onsessioninitialized: (sid) => {
|
|
500
|
+
logger3.info("HTTP session initialized", {
|
|
501
|
+
code: "HTTP_SESSION_INIT",
|
|
502
|
+
sessionId: sid
|
|
503
|
+
});
|
|
504
|
+
state.transports.set(sid, newTransport);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
newTransport.onclose = () => {
|
|
508
|
+
const sid = newTransport.sessionId;
|
|
509
|
+
if (sid !== void 0 && state.transports.has(sid)) {
|
|
510
|
+
logger3.info("HTTP transport closed", {
|
|
511
|
+
code: "HTTP_SESSION_CLOSE",
|
|
512
|
+
sessionId: sid
|
|
513
|
+
});
|
|
514
|
+
state.transports.delete(sid);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
try {
|
|
518
|
+
await server.connect(
|
|
519
|
+
newTransport
|
|
520
|
+
);
|
|
521
|
+
} catch {
|
|
522
|
+
await server.close();
|
|
523
|
+
await server.connect(
|
|
524
|
+
newTransport
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
await newTransport.handleRequest(
|
|
528
|
+
asIncoming(req),
|
|
529
|
+
asServerResponse(res),
|
|
530
|
+
req.body
|
|
531
|
+
);
|
|
532
|
+
return;
|
|
533
|
+
} else {
|
|
534
|
+
res.status(400).json({
|
|
535
|
+
jsonrpc: "2.0",
|
|
536
|
+
error: {
|
|
537
|
+
code: JSONRPC_SERVER_ERROR,
|
|
538
|
+
message: "Bad Request: No valid session ID provided"
|
|
539
|
+
},
|
|
540
|
+
id: null
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (httpTransport !== void 0) {
|
|
545
|
+
await httpTransport.handleRequest(
|
|
546
|
+
asIncoming(req),
|
|
547
|
+
asServerResponse(res),
|
|
548
|
+
req.body
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
} catch (error) {
|
|
552
|
+
logger3.error("Error handling MCP request", {
|
|
553
|
+
code: ERROR_CODES.SERVER.TRANSPORT_ERROR.full,
|
|
554
|
+
error: error instanceof Error ? error : void 0
|
|
555
|
+
});
|
|
556
|
+
if (!res.headersSent) {
|
|
557
|
+
res.status(500).json({
|
|
558
|
+
jsonrpc: "2.0",
|
|
559
|
+
error: {
|
|
560
|
+
code: JSONRPC_INTERNAL_ERROR,
|
|
561
|
+
message: "Internal server error"
|
|
562
|
+
},
|
|
563
|
+
id: null
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
})();
|
|
568
|
+
});
|
|
569
|
+
state.app.get("/mcp", (req, res) => {
|
|
570
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
571
|
+
if (sessionId === void 0 || !state.transports.has(sessionId)) {
|
|
572
|
+
res.status(400).send("Invalid or missing session ID");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const lastEventId = req.headers["last-event-id"];
|
|
576
|
+
if (lastEventId !== void 0) {
|
|
577
|
+
logger3.debug("Client reconnecting with Last-Event-ID", {
|
|
578
|
+
code: "HTTP_SSE_RECONNECT",
|
|
579
|
+
sessionId,
|
|
580
|
+
lastEventId
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
const httpTransport = state.transports.get(sessionId);
|
|
584
|
+
if (httpTransport !== void 0) {
|
|
585
|
+
void httpTransport.handleRequest(asIncoming(req), asServerResponse(res));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
state.app.delete("/mcp", (req, res) => {
|
|
589
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
590
|
+
if (sessionId === void 0 || !state.transports.has(sessionId)) {
|
|
591
|
+
res.status(400).send("Invalid or missing session ID");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
logger3.info("Session termination requested", {
|
|
595
|
+
code: "HTTP_SESSION_DELETE",
|
|
596
|
+
sessionId
|
|
597
|
+
});
|
|
598
|
+
const httpTransport = state.transports.get(sessionId);
|
|
599
|
+
if (httpTransport !== void 0) {
|
|
600
|
+
void httpTransport.handleRequest(asIncoming(req), asServerResponse(res));
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function setupLegacySSEEndpoints(state) {
|
|
605
|
+
if (!state.app || !state.mcpServer) return;
|
|
606
|
+
const server = state.mcpServer;
|
|
607
|
+
state.app.get("/sse", (req, res) => {
|
|
608
|
+
logger3.info("Legacy SSE connection requested", {
|
|
609
|
+
code: "SSE_CONNECT"
|
|
610
|
+
});
|
|
611
|
+
const sseTransport = new SSEServerTransport(
|
|
612
|
+
"/messages",
|
|
613
|
+
asServerResponse(res)
|
|
614
|
+
);
|
|
615
|
+
void (async () => {
|
|
616
|
+
try {
|
|
617
|
+
state.sseTransports.set(sseTransport.sessionId, sseTransport);
|
|
618
|
+
sseTransport.onclose = () => {
|
|
619
|
+
logger3.info("Legacy SSE transport closed", {
|
|
620
|
+
code: "SSE_CLOSE",
|
|
621
|
+
sessionId: sseTransport.sessionId
|
|
622
|
+
});
|
|
623
|
+
state.sseTransports.delete(sseTransport.sessionId);
|
|
624
|
+
};
|
|
625
|
+
try {
|
|
626
|
+
await server.connect(
|
|
627
|
+
sseTransport
|
|
628
|
+
);
|
|
629
|
+
} catch {
|
|
630
|
+
await server.close();
|
|
631
|
+
await server.connect(
|
|
632
|
+
sseTransport
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
} catch (error) {
|
|
636
|
+
logger3.error("Error starting SSE transport", {
|
|
637
|
+
code: ERROR_CODES.SERVER.TRANSPORT_ERROR.full,
|
|
638
|
+
error: error instanceof Error ? error : void 0
|
|
639
|
+
});
|
|
640
|
+
if (!res.headersSent) {
|
|
641
|
+
res.status(500).end();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
})();
|
|
645
|
+
req.on("close", () => {
|
|
646
|
+
state.sseTransports.delete(sseTransport.sessionId);
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
state.app.post("/messages", (req, res) => {
|
|
650
|
+
const sessionId = typeof req.query["sessionId"] === "string" ? req.query["sessionId"] : void 0;
|
|
651
|
+
if (!sessionId) {
|
|
652
|
+
res.status(400).json({
|
|
653
|
+
jsonrpc: "2.0",
|
|
654
|
+
error: {
|
|
655
|
+
code: JSONRPC_SERVER_ERROR,
|
|
656
|
+
message: "Missing sessionId parameter"
|
|
657
|
+
},
|
|
658
|
+
id: null
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const sseTransport = state.sseTransports.get(sessionId);
|
|
663
|
+
if (!sseTransport) {
|
|
664
|
+
res.status(404).json({
|
|
665
|
+
jsonrpc: "2.0",
|
|
666
|
+
error: {
|
|
667
|
+
code: JSONRPC_SERVER_ERROR,
|
|
668
|
+
message: "No transport found for sessionId"
|
|
669
|
+
},
|
|
670
|
+
id: null
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
void sseTransport.handlePostMessage(
|
|
675
|
+
asIncoming(req),
|
|
676
|
+
asServerResponse(res),
|
|
677
|
+
req.body
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/auth/oauth-resource-server.ts
|
|
683
|
+
var logger4 = createModuleLogger("AUTH");
|
|
684
|
+
var OAuthResourceServer = class {
|
|
685
|
+
config;
|
|
686
|
+
metadata = null;
|
|
687
|
+
constructor(config) {
|
|
688
|
+
this.config = {
|
|
689
|
+
resource: config.resource,
|
|
690
|
+
authorizationServers: config.authorizationServers,
|
|
691
|
+
scopesSupported: config.scopesSupported.length > 0 ? config.scopesSupported : [...SUPPORTED_SCOPES],
|
|
692
|
+
bearerMethodsSupported: config.bearerMethodsSupported ?? ["header"]
|
|
693
|
+
};
|
|
694
|
+
logger4.info(
|
|
695
|
+
`OAuth Resource Server initialized for: ${this.config.resource}`,
|
|
696
|
+
{ code: "AUTH_INIT" }
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Get the Protected Resource Metadata document
|
|
701
|
+
*
|
|
702
|
+
* @returns RFC 9728 compliant metadata
|
|
703
|
+
*/
|
|
704
|
+
getMetadata() {
|
|
705
|
+
this.metadata ??= this.buildMetadata();
|
|
706
|
+
return this.metadata;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Build the Protected Resource Metadata document
|
|
710
|
+
*/
|
|
711
|
+
buildMetadata() {
|
|
712
|
+
return {
|
|
713
|
+
resource: this.config.resource,
|
|
714
|
+
authorization_servers: this.config.authorizationServers,
|
|
715
|
+
scopes_supported: this.config.scopesSupported,
|
|
716
|
+
bearer_methods_supported: this.config.bearerMethodsSupported,
|
|
717
|
+
resource_documentation: `${this.config.resource}/docs`,
|
|
718
|
+
resource_signing_alg_values_supported: ["RS256", "ES256"]
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Get Express request handler for the metadata endpoint
|
|
723
|
+
*
|
|
724
|
+
* Serves: GET /.well-known/oauth-protected-resource
|
|
725
|
+
*/
|
|
726
|
+
getMetadataHandler() {
|
|
727
|
+
return (_req, res) => {
|
|
728
|
+
const metadata = this.getMetadata();
|
|
729
|
+
res.setHeader("Content-Type", "application/json");
|
|
730
|
+
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
731
|
+
res.json(metadata);
|
|
732
|
+
logger4.info("Protected Resource Metadata served", {
|
|
733
|
+
code: "AUTH_METADATA_SERVED"
|
|
734
|
+
});
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Generate WWW-Authenticate header for 401 responses
|
|
739
|
+
*
|
|
740
|
+
* @param error - Error type for the header
|
|
741
|
+
* @param errorDescription - Human-readable error description
|
|
742
|
+
* @returns WWW-Authenticate header value
|
|
743
|
+
*/
|
|
744
|
+
getWWWAuthenticateHeader(error, errorDescription) {
|
|
745
|
+
const parts = [`Bearer realm="${this.config.resource}"`];
|
|
746
|
+
if (error) {
|
|
747
|
+
parts.push(`error="${error}"`);
|
|
748
|
+
}
|
|
749
|
+
if (errorDescription) {
|
|
750
|
+
parts.push(`error_description="${errorDescription}"`);
|
|
751
|
+
}
|
|
752
|
+
return parts.join(", ");
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Get the resource URI
|
|
756
|
+
*/
|
|
757
|
+
getResourceUri() {
|
|
758
|
+
return this.config.resource;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Get the authorization servers
|
|
762
|
+
*/
|
|
763
|
+
getAuthorizationServers() {
|
|
764
|
+
return [...this.config.authorizationServers];
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Get supported scopes
|
|
768
|
+
*/
|
|
769
|
+
getSupportedScopes() {
|
|
770
|
+
return [...this.config.scopesSupported];
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Check if a scope is supported by this resource server.
|
|
774
|
+
* Validates standard scopes and dynamic patterns (db:*, table:*:*).
|
|
775
|
+
*/
|
|
776
|
+
isScopeSupported(scope) {
|
|
777
|
+
if (BASE_SCOPES.includes(scope)) {
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
if (this.config.scopesSupported.includes(scope)) {
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
if (SCOPE_PATTERNS.DATABASE.test(scope)) {
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
if (SCOPE_PATTERNS.TABLE.test(scope)) {
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Get the well-known metadata endpoint path
|
|
793
|
+
*/
|
|
794
|
+
getWellKnownPath() {
|
|
795
|
+
return "/.well-known/oauth-protected-resource";
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Clear cached metadata (useful when configuration changes)
|
|
799
|
+
*/
|
|
800
|
+
clearCache() {
|
|
801
|
+
this.metadata = null;
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// src/auth/authorization-server-discovery.ts
|
|
806
|
+
var logger5 = createModuleLogger("AUTH");
|
|
807
|
+
var AuthorizationServerDiscovery = class {
|
|
808
|
+
authServerUrl;
|
|
809
|
+
cacheTtl;
|
|
810
|
+
timeout;
|
|
811
|
+
cachedMetadata = null;
|
|
812
|
+
cacheExpiry = 0;
|
|
813
|
+
constructor(config) {
|
|
814
|
+
this.authServerUrl = config.authServerUrl.replace(/\/+$/, "");
|
|
815
|
+
this.cacheTtl = config.cacheTtl ?? 3600;
|
|
816
|
+
this.timeout = config.timeout ?? 5e3;
|
|
817
|
+
logger5.info(
|
|
818
|
+
`Authorization Server Discovery initialized for: ${this.authServerUrl}`,
|
|
819
|
+
{ code: "AUTH_INIT" }
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Discover authorization server metadata
|
|
824
|
+
*
|
|
825
|
+
* Fetches from /.well-known/oauth-authorization-server
|
|
826
|
+
* Results are cached for cacheTtl seconds.
|
|
827
|
+
*
|
|
828
|
+
* @returns Authorization server metadata
|
|
829
|
+
* @throws AuthServerDiscoveryError if discovery fails
|
|
830
|
+
*/
|
|
831
|
+
async discover() {
|
|
832
|
+
if (this.cachedMetadata && Date.now() < this.cacheExpiry) {
|
|
833
|
+
logger5.info("Using cached authorization server metadata", {
|
|
834
|
+
code: "AUTH_CACHE_HIT"
|
|
835
|
+
});
|
|
836
|
+
return this.cachedMetadata;
|
|
837
|
+
}
|
|
838
|
+
const metadataUrl = `${this.authServerUrl}/.well-known/oauth-authorization-server`;
|
|
839
|
+
logger5.info(`Fetching authorization server metadata from: ${metadataUrl}`, {
|
|
840
|
+
code: "AUTH_DISCOVERY"
|
|
841
|
+
});
|
|
842
|
+
try {
|
|
843
|
+
const controller = new AbortController();
|
|
844
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
845
|
+
const response = await fetch(metadataUrl, {
|
|
846
|
+
method: "GET",
|
|
847
|
+
headers: {
|
|
848
|
+
Accept: "application/json"
|
|
849
|
+
},
|
|
850
|
+
signal: controller.signal
|
|
851
|
+
});
|
|
852
|
+
clearTimeout(timeoutId);
|
|
853
|
+
if (!response.ok) {
|
|
854
|
+
throw new DbMcpError(
|
|
855
|
+
`HTTP ${String(response.status)}: ${response.statusText}`,
|
|
856
|
+
ERROR_CODES.AUTH.DISCOVERY_FAILED.full,
|
|
857
|
+
"connection" /* CONNECTION */
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
const metadata = await response.json();
|
|
861
|
+
this.validateMetadata(metadata);
|
|
862
|
+
this.cachedMetadata = metadata;
|
|
863
|
+
this.cacheExpiry = Date.now() + this.cacheTtl * 1e3;
|
|
864
|
+
logger5.info(
|
|
865
|
+
`Authorization server metadata cached for ${String(this.cacheTtl)}s`,
|
|
866
|
+
{ code: "AUTH_DISCOVERY_SUCCESS" }
|
|
867
|
+
);
|
|
868
|
+
return metadata;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
871
|
+
logger5.error(
|
|
872
|
+
`Failed to discover authorization server: ${this.authServerUrl}`,
|
|
873
|
+
{
|
|
874
|
+
code: ERROR_CODES.AUTH.DISCOVERY_FAILED.full,
|
|
875
|
+
operation: "discover",
|
|
876
|
+
entityId: this.authServerUrl,
|
|
877
|
+
error: cause
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
throw new AuthServerDiscoveryError(this.authServerUrl, cause);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Validate required metadata fields per RFC 8414
|
|
885
|
+
*/
|
|
886
|
+
validateMetadata(metadata) {
|
|
887
|
+
if (!metadata.issuer) {
|
|
888
|
+
throw new DbMcpError(
|
|
889
|
+
"Missing required field: issuer",
|
|
890
|
+
"AUTH_DISCOVERY_INVALID",
|
|
891
|
+
"validation" /* VALIDATION */
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
if (!metadata.token_endpoint) {
|
|
895
|
+
throw new DbMcpError(
|
|
896
|
+
"Missing required field: token_endpoint",
|
|
897
|
+
"AUTH_DISCOVERY_INVALID",
|
|
898
|
+
"validation" /* VALIDATION */
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
const expectedIssuer = this.authServerUrl;
|
|
902
|
+
if (metadata.issuer !== expectedIssuer) {
|
|
903
|
+
logger5.warning(
|
|
904
|
+
`Issuer mismatch: expected ${expectedIssuer}, got ${metadata.issuer}`,
|
|
905
|
+
{ code: "AUTH_ISSUER_MISMATCH" }
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Get cached metadata (throws if not discovered)
|
|
911
|
+
*/
|
|
912
|
+
getMetadata() {
|
|
913
|
+
if (!this.cachedMetadata) {
|
|
914
|
+
throw new DbMcpError(
|
|
915
|
+
"Authorization server metadata not yet discovered. Call discover() first.",
|
|
916
|
+
"AUTH_DISCOVERY_REQUIRED",
|
|
917
|
+
"internal" /* INTERNAL */
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
return this.cachedMetadata;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Get JWKS URI from metadata
|
|
924
|
+
*
|
|
925
|
+
* @throws Error if metadata not discovered or jwks_uri not present
|
|
926
|
+
*/
|
|
927
|
+
getJwksUri() {
|
|
928
|
+
const metadata = this.getMetadata();
|
|
929
|
+
if (!metadata.jwks_uri) {
|
|
930
|
+
throw new DbMcpError(
|
|
931
|
+
"Authorization server does not provide jwks_uri",
|
|
932
|
+
"AUTH_DISCOVERY_INVALID",
|
|
933
|
+
"validation" /* VALIDATION */
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return metadata.jwks_uri;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Get token endpoint from metadata
|
|
940
|
+
*/
|
|
941
|
+
getTokenEndpoint() {
|
|
942
|
+
return this.getMetadata().token_endpoint;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Get issuer from metadata
|
|
946
|
+
*/
|
|
947
|
+
getIssuer() {
|
|
948
|
+
return this.getMetadata().issuer;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Get registration endpoint from metadata (RFC 7591)
|
|
952
|
+
*
|
|
953
|
+
* @returns Registration endpoint or null if not supported
|
|
954
|
+
*/
|
|
955
|
+
getRegistrationEndpoint() {
|
|
956
|
+
return this.getMetadata().registration_endpoint ?? null;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Check if dynamic client registration is supported
|
|
960
|
+
*/
|
|
961
|
+
supportsClientRegistration() {
|
|
962
|
+
return this.getRegistrationEndpoint() !== null;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Get supported scopes from metadata
|
|
966
|
+
*/
|
|
967
|
+
getSupportedScopes() {
|
|
968
|
+
return this.getMetadata().scopes_supported ?? [];
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Check if a specific scope is supported
|
|
972
|
+
*/
|
|
973
|
+
isScopeSupported(scope) {
|
|
974
|
+
const supportedScopes = this.getSupportedScopes();
|
|
975
|
+
return supportedScopes.length === 0 || supportedScopes.includes(scope);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Clear cached metadata
|
|
979
|
+
*/
|
|
980
|
+
clearCache() {
|
|
981
|
+
this.cachedMetadata = null;
|
|
982
|
+
this.cacheExpiry = 0;
|
|
983
|
+
logger5.info("Authorization server metadata cache cleared", {
|
|
984
|
+
code: "AUTH_CACHE_CLEARED"
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Check if cache is valid
|
|
989
|
+
*/
|
|
990
|
+
isCacheValid() {
|
|
991
|
+
return this.cachedMetadata !== null && Date.now() < this.cacheExpiry;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Get the authorization server URL
|
|
995
|
+
*/
|
|
996
|
+
getAuthServerUrl() {
|
|
997
|
+
return this.authServerUrl;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
var logger6 = createModuleLogger("AUTH");
|
|
1001
|
+
var TokenValidator = class {
|
|
1002
|
+
/** Resolved configuration with all defaults applied */
|
|
1003
|
+
jwksUri;
|
|
1004
|
+
issuer;
|
|
1005
|
+
audience;
|
|
1006
|
+
clockTolerance;
|
|
1007
|
+
jwksCacheTtl;
|
|
1008
|
+
jwks = null;
|
|
1009
|
+
jwksExpiry = 0;
|
|
1010
|
+
constructor(config) {
|
|
1011
|
+
this.jwksUri = config.jwksUri;
|
|
1012
|
+
this.issuer = config.issuer;
|
|
1013
|
+
this.audience = config.audience;
|
|
1014
|
+
this.clockTolerance = config.clockTolerance ?? 60;
|
|
1015
|
+
this.jwksCacheTtl = config.jwksCacheTtl ?? 3600;
|
|
1016
|
+
logger6.info(`Token Validator initialized for issuer: ${this.issuer}`, {
|
|
1017
|
+
code: "AUTH_INIT"
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Validate an access token
|
|
1022
|
+
*
|
|
1023
|
+
* @param token - The JWT access token
|
|
1024
|
+
* @returns Validation result with claims or error
|
|
1025
|
+
*/
|
|
1026
|
+
async validate(token) {
|
|
1027
|
+
try {
|
|
1028
|
+
const jwks = this.getJwks();
|
|
1029
|
+
const { payload } = await jose.jwtVerify(token, jwks, {
|
|
1030
|
+
issuer: this.issuer,
|
|
1031
|
+
audience: this.audience,
|
|
1032
|
+
clockTolerance: this.clockTolerance
|
|
1033
|
+
});
|
|
1034
|
+
const claims = this.extractClaims(payload);
|
|
1035
|
+
logger6.info(`Token validated for subject: ${claims.sub}`, {
|
|
1036
|
+
code: "AUTH_TOKEN_VALID",
|
|
1037
|
+
sub: claims.sub,
|
|
1038
|
+
scopes: claims.scopes.length,
|
|
1039
|
+
exp: new Date(claims.exp * 1e3).toISOString()
|
|
1040
|
+
});
|
|
1041
|
+
return {
|
|
1042
|
+
valid: true,
|
|
1043
|
+
claims
|
|
1044
|
+
};
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
return this.handleValidationError(error);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Get or refresh the JWKS
|
|
1051
|
+
*/
|
|
1052
|
+
getJwks() {
|
|
1053
|
+
if (this.jwks && Date.now() < this.jwksExpiry) {
|
|
1054
|
+
return this.jwks;
|
|
1055
|
+
}
|
|
1056
|
+
logger6.info(`Fetching JWKS from: ${this.jwksUri}`, {
|
|
1057
|
+
code: "AUTH_JWKS_FETCH"
|
|
1058
|
+
});
|
|
1059
|
+
try {
|
|
1060
|
+
this.jwks = jose.createRemoteJWKSet(new URL(this.jwksUri), {
|
|
1061
|
+
cooldownDuration: 3e4,
|
|
1062
|
+
// 30 seconds between retries
|
|
1063
|
+
cacheMaxAge: this.jwksCacheTtl * 1e3
|
|
1064
|
+
});
|
|
1065
|
+
this.jwksExpiry = Date.now() + this.jwksCacheTtl * 1e3;
|
|
1066
|
+
logger6.info(`JWKS cached for ${String(this.jwksCacheTtl)}s`, {
|
|
1067
|
+
code: "AUTH_JWKS_CACHED"
|
|
1068
|
+
});
|
|
1069
|
+
return this.jwks;
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
1072
|
+
logger6.error(`Failed to fetch JWKS: ${this.jwksUri}`, {
|
|
1073
|
+
code: ERROR_CODES.AUTH.JWKS_FETCH_FAILED.full,
|
|
1074
|
+
error: cause
|
|
1075
|
+
});
|
|
1076
|
+
throw new JwksFetchError(this.jwksUri, cause);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Extract and normalize token claims
|
|
1081
|
+
*/
|
|
1082
|
+
extractClaims(payload) {
|
|
1083
|
+
let scopes = [];
|
|
1084
|
+
if (typeof payload["scope"] === "string") {
|
|
1085
|
+
scopes = parseScopes(payload["scope"]);
|
|
1086
|
+
} else if (Array.isArray(payload["scopes"])) {
|
|
1087
|
+
scopes = payload["scopes"].filter(
|
|
1088
|
+
(s) => typeof s === "string"
|
|
1089
|
+
);
|
|
1090
|
+
} else if (Array.isArray(payload["scope"])) {
|
|
1091
|
+
scopes = payload["scope"].filter(
|
|
1092
|
+
(s) => typeof s === "string"
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
return {
|
|
1096
|
+
sub: payload.sub ?? "unknown",
|
|
1097
|
+
scopes,
|
|
1098
|
+
exp: payload.exp ?? 0,
|
|
1099
|
+
iat: payload.iat ?? 0,
|
|
1100
|
+
iss: payload.iss,
|
|
1101
|
+
aud: payload.aud,
|
|
1102
|
+
nbf: payload.nbf ?? void 0,
|
|
1103
|
+
jti: payload.jti,
|
|
1104
|
+
client_id: payload["client_id"],
|
|
1105
|
+
// Include all other claims
|
|
1106
|
+
...payload
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Handle validation errors and convert to TokenValidationResult
|
|
1111
|
+
*/
|
|
1112
|
+
handleValidationError(error) {
|
|
1113
|
+
if (error instanceof jose.errors.JWTExpired) {
|
|
1114
|
+
logger6.warning("Token has expired", {
|
|
1115
|
+
code: ERROR_CODES.AUTH.TOKEN_EXPIRED.full,
|
|
1116
|
+
error
|
|
1117
|
+
});
|
|
1118
|
+
return {
|
|
1119
|
+
valid: false,
|
|
1120
|
+
error: "Token has expired",
|
|
1121
|
+
errorCode: ERROR_CODES.AUTH.TOKEN_EXPIRED.full
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
if (error instanceof jose.errors.JWTClaimValidationFailed) {
|
|
1125
|
+
logger6.warning(`Token claim validation failed: ${error.message}`, {
|
|
1126
|
+
code: ERROR_CODES.AUTH.TOKEN_INVALID.full,
|
|
1127
|
+
error
|
|
1128
|
+
});
|
|
1129
|
+
return {
|
|
1130
|
+
valid: false,
|
|
1131
|
+
error: `Token claim validation failed: ${error.message}`,
|
|
1132
|
+
errorCode: ERROR_CODES.AUTH.TOKEN_INVALID.full
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
|
|
1136
|
+
logger6.warning("Token signature verification failed", {
|
|
1137
|
+
code: ERROR_CODES.AUTH.SIGNATURE_INVALID.full,
|
|
1138
|
+
error
|
|
1139
|
+
});
|
|
1140
|
+
return {
|
|
1141
|
+
valid: false,
|
|
1142
|
+
error: "Token signature verification failed",
|
|
1143
|
+
errorCode: ERROR_CODES.AUTH.SIGNATURE_INVALID.full
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
if (error instanceof jose.errors.JWKSNoMatchingKey) {
|
|
1147
|
+
logger6.warning("No matching key found in JWKS", {
|
|
1148
|
+
code: ERROR_CODES.AUTH.TOKEN_INVALID.full,
|
|
1149
|
+
error
|
|
1150
|
+
});
|
|
1151
|
+
return {
|
|
1152
|
+
valid: false,
|
|
1153
|
+
error: "No matching key found in JWKS",
|
|
1154
|
+
errorCode: ERROR_CODES.AUTH.TOKEN_INVALID.full
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1158
|
+
logger6.error(`Token validation failed: ${message}`, {
|
|
1159
|
+
code: ERROR_CODES.AUTH.TOKEN_INVALID.full,
|
|
1160
|
+
error: error instanceof Error ? error : void 0
|
|
1161
|
+
});
|
|
1162
|
+
return {
|
|
1163
|
+
valid: false,
|
|
1164
|
+
error: `Token validation failed: ${message}`,
|
|
1165
|
+
errorCode: ERROR_CODES.AUTH.TOKEN_INVALID.full
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Refresh the JWKS cache
|
|
1170
|
+
*/
|
|
1171
|
+
refreshJwks() {
|
|
1172
|
+
this.jwks = null;
|
|
1173
|
+
this.jwksExpiry = 0;
|
|
1174
|
+
this.getJwks();
|
|
1175
|
+
logger6.info("JWKS cache refreshed", { code: "AUTH_JWKS_REFRESHED" });
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Clear the JWKS cache
|
|
1179
|
+
*/
|
|
1180
|
+
clearCache() {
|
|
1181
|
+
this.jwks = null;
|
|
1182
|
+
this.jwksExpiry = 0;
|
|
1183
|
+
logger6.info("Token validator cache cleared", {
|
|
1184
|
+
code: "AUTH_CACHE_CLEARED"
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Convert a validation error to the appropriate OAuth error class
|
|
1189
|
+
*/
|
|
1190
|
+
static toOAuthError(result) {
|
|
1191
|
+
if (result.errorCode === ERROR_CODES.AUTH.TOKEN_EXPIRED.full) {
|
|
1192
|
+
return new TokenExpiredError();
|
|
1193
|
+
}
|
|
1194
|
+
if (result.errorCode === ERROR_CODES.AUTH.SIGNATURE_INVALID.full) {
|
|
1195
|
+
return new InvalidSignatureError();
|
|
1196
|
+
}
|
|
1197
|
+
return new InvalidTokenError(result.error);
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
// src/transports/http/oauth.ts
|
|
1202
|
+
var logger7 = createModuleLogger("HTTP");
|
|
1203
|
+
function applyAuthMiddleware(state) {
|
|
1204
|
+
if (state.config.oauth.enabled && state.tokenValidator && state.resourceServer && state.app) {
|
|
1205
|
+
const authMiddleware = createAuthMiddleware({
|
|
1206
|
+
tokenValidator: state.tokenValidator,
|
|
1207
|
+
resourceServer: state.resourceServer,
|
|
1208
|
+
publicPaths: ["/health", ...state.config.oauth.publicPaths ?? []]
|
|
1209
|
+
});
|
|
1210
|
+
state.app.use(authMiddleware);
|
|
1211
|
+
} else if (state.config.authToken && state.app) {
|
|
1212
|
+
state.app.use(createSimpleBearerAuth(state.config.authToken));
|
|
1213
|
+
logger7.info("Simple bearer token authentication enabled", {
|
|
1214
|
+
code: "AUTH_BEARER_ENABLED"
|
|
1215
|
+
});
|
|
1216
|
+
} else if (state.app) {
|
|
1217
|
+
logger7.warning(
|
|
1218
|
+
"No authentication configured for HTTP transport. Set --auth-token or --oauth-enabled for production deployments.",
|
|
1219
|
+
{ code: "AUTH_NONE" }
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
function createSimpleBearerAuth(expectedToken) {
|
|
1224
|
+
const publicPaths = ["/health", "/", "/.well-known"];
|
|
1225
|
+
return (req, res, next) => {
|
|
1226
|
+
if (publicPaths.some((p) => req.path === p || req.path.startsWith(`${p}/`))) {
|
|
1227
|
+
next();
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const authHeader = req.headers.authorization;
|
|
1231
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
1232
|
+
res.status(401);
|
|
1233
|
+
res.setHeader("WWW-Authenticate", 'Bearer realm="db-mcp"');
|
|
1234
|
+
res.json({
|
|
1235
|
+
error: "unauthorized",
|
|
1236
|
+
error_description: "Bearer token required"
|
|
1237
|
+
});
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const token = authHeader.slice(7);
|
|
1241
|
+
if (token !== expectedToken) {
|
|
1242
|
+
logger7.warning("Invalid bearer token rejected", {
|
|
1243
|
+
code: "AUTH_BEARER_REJECTED"
|
|
1244
|
+
});
|
|
1245
|
+
res.status(401);
|
|
1246
|
+
res.setHeader(
|
|
1247
|
+
"WWW-Authenticate",
|
|
1248
|
+
'Bearer realm="db-mcp", error="invalid_token"'
|
|
1249
|
+
);
|
|
1250
|
+
res.json({
|
|
1251
|
+
error: "unauthorized",
|
|
1252
|
+
error_description: "Invalid bearer token"
|
|
1253
|
+
});
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
next();
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
async function setupOAuth(state, resourceUri) {
|
|
1260
|
+
logger7.info("Setting up OAuth 2.1...", { code: "HTTP_OAUTH_SETUP" });
|
|
1261
|
+
state.resourceServer = new OAuthResourceServer({
|
|
1262
|
+
resource: resourceUri,
|
|
1263
|
+
authorizationServers: [state.config.oauth.authorizationServerUrl],
|
|
1264
|
+
scopesSupported: [...SUPPORTED_SCOPES]
|
|
1265
|
+
});
|
|
1266
|
+
state.app?.get(
|
|
1267
|
+
"/.well-known/oauth-protected-resource",
|
|
1268
|
+
state.resourceServer.getMetadataHandler()
|
|
1269
|
+
);
|
|
1270
|
+
state.authServerDiscovery = new AuthorizationServerDiscovery({
|
|
1271
|
+
authServerUrl: state.config.oauth.authorizationServerUrl
|
|
1272
|
+
});
|
|
1273
|
+
try {
|
|
1274
|
+
const metadata = await state.authServerDiscovery.discover();
|
|
1275
|
+
state.tokenValidator = new TokenValidator({
|
|
1276
|
+
jwksUri: state.config.oauth.jwksUri ?? metadata.jwks_uri ?? "",
|
|
1277
|
+
issuer: state.config.oauth.issuer ?? metadata.issuer,
|
|
1278
|
+
audience: state.config.oauth.audience,
|
|
1279
|
+
clockTolerance: state.config.oauth.clockTolerance
|
|
1280
|
+
});
|
|
1281
|
+
logger7.info("OAuth 2.1 setup complete", { code: "HTTP_OAUTH_READY" });
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
logger7.warning(
|
|
1284
|
+
"Authorization server discovery failed. OAuth validation disabled.",
|
|
1285
|
+
{
|
|
1286
|
+
code: ERROR_CODES.AUTH.DISCOVERY_FAILED.full,
|
|
1287
|
+
error: error instanceof Error ? error : void 0
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
if (!state.config.oauth.jwksUri) {
|
|
1291
|
+
logger7.error(
|
|
1292
|
+
"No JWKS URI available. Please provide oauth.jwksUri in config.",
|
|
1293
|
+
{ code: ERROR_CODES.AUTH.DISCOVERY_FAILED.full }
|
|
1294
|
+
);
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1297
|
+
state.tokenValidator = new TokenValidator({
|
|
1298
|
+
jwksUri: state.config.oauth.jwksUri,
|
|
1299
|
+
issuer: state.config.oauth.issuer ?? state.config.oauth.authorizationServerUrl,
|
|
1300
|
+
audience: state.config.oauth.audience,
|
|
1301
|
+
clockTolerance: state.config.oauth.clockTolerance
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/transports/http/transport.ts
|
|
1307
|
+
var logger8 = createModuleLogger("HTTP");
|
|
1308
|
+
var HttpTransport = class {
|
|
1309
|
+
/** Internal state shared with session/middleware/oauth modules */
|
|
1310
|
+
state;
|
|
1311
|
+
constructor(config) {
|
|
1312
|
+
this.state = {
|
|
1313
|
+
config: {
|
|
1314
|
+
...config,
|
|
1315
|
+
host: config.host ?? "0.0.0.0",
|
|
1316
|
+
stateless: config.stateless ?? false,
|
|
1317
|
+
enableHSTS: config.enableHSTS ?? process.env["MCP_ENABLE_HSTS"] === "true"
|
|
1318
|
+
},
|
|
1319
|
+
app: null,
|
|
1320
|
+
httpServer: null,
|
|
1321
|
+
transports: /* @__PURE__ */ new Map(),
|
|
1322
|
+
sseTransports: /* @__PURE__ */ new Map(),
|
|
1323
|
+
statelessTransport: null,
|
|
1324
|
+
resourceServer: null,
|
|
1325
|
+
authServerDiscovery: null,
|
|
1326
|
+
tokenValidator: null,
|
|
1327
|
+
mcpServer: null,
|
|
1328
|
+
rateLimitMap: /* @__PURE__ */ new Map(),
|
|
1329
|
+
rateLimitCleanupTimer: null
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Initialize the transport with the MCP server.
|
|
1334
|
+
*
|
|
1335
|
+
* For stateful mode, the server reference is stored to connect new sessions.
|
|
1336
|
+
* For stateless mode, a single transport is created and connected.
|
|
1337
|
+
*
|
|
1338
|
+
* @param server - The MCP server instance
|
|
1339
|
+
*/
|
|
1340
|
+
async initialize(server) {
|
|
1341
|
+
logger8.info("Initializing HTTP transport...", {
|
|
1342
|
+
code: "HTTP_INIT",
|
|
1343
|
+
stateless: this.state.config.stateless
|
|
1344
|
+
});
|
|
1345
|
+
this.state.mcpServer = server;
|
|
1346
|
+
this.state.app = express();
|
|
1347
|
+
this.state.app.use(localhostHostValidation());
|
|
1348
|
+
setupSecurityHeaders(this.state);
|
|
1349
|
+
setupCors(this.state);
|
|
1350
|
+
setupRateLimiting(this.state);
|
|
1351
|
+
const maxBodyBytes = this.state.config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
1352
|
+
this.state.app.use(
|
|
1353
|
+
express.json({ limit: maxBodyBytes })
|
|
1354
|
+
);
|
|
1355
|
+
const resourceUri = this.state.config.resourceUri ?? `http://localhost:${String(this.state.config.port)}`;
|
|
1356
|
+
if (this.state.config.oauth.enabled) {
|
|
1357
|
+
await setupOAuth(this.state, resourceUri);
|
|
1358
|
+
}
|
|
1359
|
+
this.state.app.get("/health", (_req, res) => {
|
|
1360
|
+
res.json({
|
|
1361
|
+
status: "healthy",
|
|
1362
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1363
|
+
oauth: this.state.config.oauth.enabled,
|
|
1364
|
+
mode: this.state.config.stateless ? "stateless" : "stateful",
|
|
1365
|
+
activeSessions: this.state.transports.size
|
|
1366
|
+
});
|
|
1367
|
+
});
|
|
1368
|
+
this.state.app.get("/", (_req, res) => {
|
|
1369
|
+
res.json({
|
|
1370
|
+
name: "db-mcp",
|
|
1371
|
+
description: "SQLite MCP Server with dual HTTP transport",
|
|
1372
|
+
mode: this.state.config.stateless ? "stateless" : "stateful",
|
|
1373
|
+
endpoints: {
|
|
1374
|
+
"POST /mcp": "JSON-RPC requests (Streamable HTTP, MCP 2025-03-26)",
|
|
1375
|
+
"GET /mcp": this.state.config.stateless === true ? "Not available in stateless mode" : "SSE stream for server-to-client notifications",
|
|
1376
|
+
"DELETE /mcp": "Session termination",
|
|
1377
|
+
"GET /sse": "Legacy SSE connection (MCP 2024-11-05)",
|
|
1378
|
+
"POST /messages": "Legacy SSE message endpoint",
|
|
1379
|
+
"GET /health": "Health check"
|
|
1380
|
+
},
|
|
1381
|
+
documentation: "https://github.com/neverinfamous/db-mcp"
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
applyAuthMiddleware(this.state);
|
|
1385
|
+
if (this.state.config.stateless) {
|
|
1386
|
+
await setupStatelessEndpoints(this.state);
|
|
1387
|
+
} else {
|
|
1388
|
+
setupStatefulEndpoints(this.state);
|
|
1389
|
+
}
|
|
1390
|
+
if (!this.state.config.stateless) {
|
|
1391
|
+
setupLegacySSEEndpoints(this.state);
|
|
1392
|
+
}
|
|
1393
|
+
this.state.app.use(oauthErrorHandler);
|
|
1394
|
+
this.state.app.use((_req, res) => {
|
|
1395
|
+
res.status(404).json({ error: "Not found" });
|
|
1396
|
+
});
|
|
1397
|
+
logger8.info("HTTP transport initialized", {
|
|
1398
|
+
code: "HTTP_INIT_COMPLETE",
|
|
1399
|
+
port: this.state.config.port,
|
|
1400
|
+
oauth: this.state.config.oauth.enabled,
|
|
1401
|
+
mode: this.state.config.stateless ? "stateless" : "stateful",
|
|
1402
|
+
resourceUri
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Start listening on the configured port
|
|
1407
|
+
*/
|
|
1408
|
+
async start() {
|
|
1409
|
+
if (!this.state.app) {
|
|
1410
|
+
throw new DbMcpError(
|
|
1411
|
+
"Transport not initialized. Call initialize() first.",
|
|
1412
|
+
ERROR_CODES.SERVER.START_FAILED.full,
|
|
1413
|
+
"internal" /* INTERNAL */
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return new Promise((resolve, reject) => {
|
|
1417
|
+
try {
|
|
1418
|
+
this.state.httpServer = this.state.app?.listen(
|
|
1419
|
+
this.state.config.port,
|
|
1420
|
+
this.state.config.host ?? "0.0.0.0",
|
|
1421
|
+
() => {
|
|
1422
|
+
if (this.state.httpServer) {
|
|
1423
|
+
this.state.httpServer.setTimeout(HTTP_REQUEST_TIMEOUT_MS);
|
|
1424
|
+
this.state.httpServer.keepAliveTimeout = HTTP_KEEP_ALIVE_TIMEOUT_MS;
|
|
1425
|
+
this.state.httpServer.headersTimeout = HTTP_HEADERS_TIMEOUT_MS;
|
|
1426
|
+
}
|
|
1427
|
+
logger8.info(
|
|
1428
|
+
`HTTP server listening on ${this.state.config.host ?? "0.0.0.0"}:${String(this.state.config.port)}`,
|
|
1429
|
+
{
|
|
1430
|
+
code: "HTTP_SERVER_STARTED",
|
|
1431
|
+
mode: this.state.config.stateless ? "stateless" : "stateful"
|
|
1432
|
+
}
|
|
1433
|
+
);
|
|
1434
|
+
resolve();
|
|
1435
|
+
}
|
|
1436
|
+
) ?? null;
|
|
1437
|
+
this.state.httpServer?.on("error", (error) => {
|
|
1438
|
+
logger8.error("Failed to start HTTP server", {
|
|
1439
|
+
code: ERROR_CODES.SERVER.START_FAILED.full,
|
|
1440
|
+
error
|
|
1441
|
+
});
|
|
1442
|
+
reject(error);
|
|
1443
|
+
});
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Stop the server and close all active sessions
|
|
1451
|
+
*/
|
|
1452
|
+
async stop() {
|
|
1453
|
+
if (this.state.rateLimitCleanupTimer) {
|
|
1454
|
+
clearInterval(this.state.rateLimitCleanupTimer);
|
|
1455
|
+
this.state.rateLimitCleanupTimer = null;
|
|
1456
|
+
}
|
|
1457
|
+
for (const [sessionId, transport] of this.state.transports) {
|
|
1458
|
+
try {
|
|
1459
|
+
logger8.debug("Closing transport", {
|
|
1460
|
+
code: "HTTP_SESSION_CLEANUP",
|
|
1461
|
+
sessionId
|
|
1462
|
+
});
|
|
1463
|
+
await transport.close();
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
logger8.error("Error closing transport", {
|
|
1466
|
+
code: ERROR_CODES.SERVER.SHUTDOWN_FAILED.full,
|
|
1467
|
+
sessionId,
|
|
1468
|
+
error: error instanceof Error ? error : void 0
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
this.state.transports.clear();
|
|
1473
|
+
for (const [sessionId, transport] of this.state.sseTransports) {
|
|
1474
|
+
try {
|
|
1475
|
+
logger8.debug("Closing SSE transport", {
|
|
1476
|
+
code: "SSE_SESSION_CLEANUP",
|
|
1477
|
+
sessionId
|
|
1478
|
+
});
|
|
1479
|
+
await transport.close();
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
logger8.error("Error closing SSE transport", {
|
|
1482
|
+
code: ERROR_CODES.SERVER.SHUTDOWN_FAILED.full,
|
|
1483
|
+
sessionId,
|
|
1484
|
+
error: error instanceof Error ? error : void 0
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
this.state.sseTransports.clear();
|
|
1489
|
+
if (this.state.statelessTransport) {
|
|
1490
|
+
try {
|
|
1491
|
+
await this.state.statelessTransport.close();
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
logger8.error("Error closing stateless transport", {
|
|
1494
|
+
code: ERROR_CODES.SERVER.SHUTDOWN_FAILED.full,
|
|
1495
|
+
error: error instanceof Error ? error : void 0
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return new Promise((resolve, reject) => {
|
|
1500
|
+
if (!this.state.httpServer) {
|
|
1501
|
+
resolve();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
this.state.httpServer.close((error) => {
|
|
1505
|
+
if (error) {
|
|
1506
|
+
logger8.error("Error stopping HTTP server", {
|
|
1507
|
+
code: ERROR_CODES.SERVER.SHUTDOWN_FAILED.full,
|
|
1508
|
+
error
|
|
1509
|
+
});
|
|
1510
|
+
reject(error);
|
|
1511
|
+
} else {
|
|
1512
|
+
logger8.info("HTTP server stopped", { code: "HTTP_SERVER_STOPPED" });
|
|
1513
|
+
resolve();
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Get the underlying Express app
|
|
1520
|
+
*/
|
|
1521
|
+
getApp() {
|
|
1522
|
+
return this.state.app;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Get the number of active sessions (stateful mode only)
|
|
1526
|
+
*/
|
|
1527
|
+
getActiveSessionCount() {
|
|
1528
|
+
return this.state.transports.size;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Get the OAuth Resource Server
|
|
1532
|
+
*/
|
|
1533
|
+
getResourceServer() {
|
|
1534
|
+
return this.state.resourceServer;
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
function createHttpTransport(config) {
|
|
1538
|
+
return new HttpTransport(config);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
export { HttpTransport, createHttpTransport };
|