@databrainhq/mcp-server 0.3.0-rc.12 → 0.3.0-rc.14
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/.mcp.json +6 -1
- package/dist/apps/server.d.ts +102 -389
- package/dist/apps/server.js +5 -5
- package/dist/apps/stdio.d.ts +1 -2
- package/dist/apps/stdio.js +6 -6
- package/dist/{bundle-TDQXWIPM.js → bundle-QJNCUMNY.js} +2 -2
- package/dist/{chunk-Q5EYDKSA.js → chunk-2F7V6RZR.js} +511 -107
- package/dist/chunk-2F7V6RZR.js.map +1 -0
- package/dist/{chunk-347U6GXR.js → chunk-AWICHHDU.js} +153 -61
- package/dist/chunk-AWICHHDU.js.map +1 -0
- package/dist/{chunk-Z3ZHN4UW.js → chunk-CEGHG5U3.js} +59 -18
- package/dist/chunk-CEGHG5U3.js.map +1 -0
- package/dist/{chunk-ZTHGF27R.js → chunk-NIC5MAGC.js} +317 -147
- package/dist/chunk-NIC5MAGC.js.map +1 -0
- package/dist/{chunk-V3UW3B6U.js → chunk-S65KA3Z7.js} +26 -11
- package/dist/chunk-S65KA3Z7.js.map +1 -0
- package/dist/{chunk-OLOC2MTA.js → chunk-UYFFFAYV.js} +11722 -10560
- package/dist/chunk-UYFFFAYV.js.map +1 -0
- package/dist/{chunk-LACJHQYH.js → chunk-VQ5G5AZV.js} +10 -7
- package/dist/chunk-VQ5G5AZV.js.map +1 -0
- package/dist/cli.d.ts +16 -15
- package/dist/cli.js +134 -36
- package/dist/cli.js.map +1 -1
- package/dist/custom-chart/custom-runtime.js +6 -6
- package/dist/custom-chart/template/chart.ts +116 -28
- package/dist/custom-chart/template/icon.svg +1 -1
- package/dist/custom-chart/template/manifest.json +66 -12
- package/dist/custom-chart/template/sample-data.json +21 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/{main-IUV24DMP.js → main-QCBVJSSR.js} +363 -155
- package/dist/main-QCBVJSSR.js.map +1 -0
- package/dist/{test-only-K7QTSVO4.js → test-only-6474NMTR.js} +12 -8
- package/dist/test-only-6474NMTR.js.map +1 -0
- package/package.json +20 -9
- package/dist/chunk-347U6GXR.js.map +0 -1
- package/dist/chunk-LACJHQYH.js.map +0 -1
- package/dist/chunk-OLOC2MTA.js.map +0 -1
- package/dist/chunk-Q5EYDKSA.js.map +0 -1
- package/dist/chunk-V3UW3B6U.js.map +0 -1
- package/dist/chunk-Z3ZHN4UW.js.map +0 -1
- package/dist/chunk-ZTHGF27R.js.map +0 -1
- package/dist/main-IUV24DMP.js.map +0 -1
- package/dist/test-only-K7QTSVO4.js.map +0 -1
- /package/dist/{bundle-TDQXWIPM.js.map → bundle-QJNCUMNY.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
registerChartApps
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NIC5MAGC.js";
|
|
5
5
|
import {
|
|
6
6
|
PREVIEW_PATH,
|
|
7
7
|
WRITE_CREATE,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
registerAllPrompts,
|
|
10
10
|
registerAllResources,
|
|
11
11
|
registerAllTools
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-UYFFFAYV.js";
|
|
13
13
|
import {
|
|
14
14
|
DatabrainApiError,
|
|
15
15
|
DatabrainClient,
|
|
@@ -20,15 +20,15 @@ import {
|
|
|
20
20
|
toolContext,
|
|
21
21
|
toolError,
|
|
22
22
|
toolSuccess
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import "./chunk-
|
|
23
|
+
} from "./chunk-AWICHHDU.js";
|
|
24
|
+
import "./chunk-2F7V6RZR.js";
|
|
25
25
|
import {
|
|
26
26
|
FilePatStore,
|
|
27
27
|
HOSTED_SCOPES,
|
|
28
28
|
HostedAuthError,
|
|
29
29
|
createBearerVerifier,
|
|
30
30
|
principalSchema
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-CEGHG5U3.js";
|
|
32
32
|
|
|
33
33
|
// src/hosted/main.ts
|
|
34
34
|
import { isAbsolute } from "path";
|
|
@@ -37,16 +37,21 @@ import { pathToFileURL } from "url";
|
|
|
37
37
|
// src/hosted/config.ts
|
|
38
38
|
function loadHostedConfig(env = process.env) {
|
|
39
39
|
const testOnly = env.DATABRAIN_HOSTED_TEST_ONLY === "1";
|
|
40
|
-
if (testOnly && env.NODE_ENV === "production")
|
|
41
|
-
|
|
40
|
+
if (testOnly && env.NODE_ENV === "production")
|
|
41
|
+
throw new Error("Hosted test mode is forbidden in production.");
|
|
42
|
+
if (env.DATABRAIN_HOSTED_TEST_ONLY && !testOnly)
|
|
43
|
+
throw new Error("Invalid DATABRAIN_HOSTED_TEST_ONLY.");
|
|
42
44
|
const validUrl = (value, field) => {
|
|
43
45
|
try {
|
|
44
46
|
if (!value) throw new Error();
|
|
45
47
|
const url = new URL(value);
|
|
46
|
-
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:")
|
|
48
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:")
|
|
49
|
+
throw new Error();
|
|
47
50
|
return url;
|
|
48
51
|
} catch {
|
|
49
|
-
throw new Error(
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Configure ${field} as an HTTPS URL without credentials, query or fragment.`
|
|
54
|
+
);
|
|
50
55
|
}
|
|
51
56
|
};
|
|
52
57
|
let publicValue = env.DATABRAIN_HOSTED_PUBLIC_URL;
|
|
@@ -58,13 +63,31 @@ function loadHostedConfig(env = process.env) {
|
|
|
58
63
|
}
|
|
59
64
|
const publicUrl = validUrl(publicValue, "DATABRAIN_HOSTED_PUBLIC_URL");
|
|
60
65
|
validUrl(issuerValue, "DATABRAIN_HOSTED_ISSUER");
|
|
61
|
-
if (publicUrl.pathname !== "/mcp")
|
|
66
|
+
if (publicUrl.pathname !== "/mcp")
|
|
67
|
+
throw new Error("DATABRAIN_HOSTED_PUBLIC_URL must end in /mcp.");
|
|
62
68
|
const port = Number(env.DATABRAIN_HOSTED_PORT || "3144");
|
|
63
69
|
const bind = env.DATABRAIN_HOSTED_BIND || "127.0.0.1";
|
|
64
|
-
if (!Number.isInteger(port) || port < 1024 || port > 65535)
|
|
70
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535)
|
|
71
|
+
throw new Error("Invalid DATABRAIN_HOSTED_PORT.");
|
|
72
|
+
let apiUrl;
|
|
73
|
+
if (env.DATABRAIN_HOSTED_API_URL) {
|
|
74
|
+
let url;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(env.DATABRAIN_HOSTED_API_URL);
|
|
77
|
+
} catch {
|
|
78
|
+
url = void 0;
|
|
79
|
+
}
|
|
80
|
+
if (!url || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Configure DATABRAIN_HOSTED_API_URL as an origin without credentials, path, query or fragment."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
apiUrl = url.origin;
|
|
86
|
+
}
|
|
65
87
|
return {
|
|
66
88
|
publicUrl: publicValue,
|
|
67
89
|
issuer: issuerValue,
|
|
90
|
+
...apiUrl ? { apiUrl } : {},
|
|
68
91
|
port,
|
|
69
92
|
bind,
|
|
70
93
|
testOnly
|
|
@@ -72,64 +95,11 @@ function loadHostedConfig(env = process.env) {
|
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
// src/hosted/http.ts
|
|
75
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
createServer
|
|
100
|
+
} from "http";
|
|
76
101
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
77
102
|
|
|
78
|
-
// src/hosted/server.ts
|
|
79
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
80
|
-
import { z as z2 } from "zod";
|
|
81
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
82
|
-
import { instrument } from "@posthog/mcp";
|
|
83
|
-
import { PostHog } from "posthog-node";
|
|
84
|
-
|
|
85
|
-
// src/tools/infrastructure/create-workspace.ts
|
|
86
|
-
import { z } from "zod";
|
|
87
|
-
function registerCreateWorkspace(server, client) {
|
|
88
|
-
server.tool(
|
|
89
|
-
"create_workspace",
|
|
90
|
-
'Create a new workspace to connect a datasource or datamart for dashboard building. Works only when this sign-in granted "All workspaces I can access" and your role may create workspaces; the new workspace is usable on the next call.',
|
|
91
|
-
{
|
|
92
|
-
name: z.string().describe("Unique workspace name"),
|
|
93
|
-
connectionType: z.enum(["DATASOURCE", "DATAMART", "MULTI_DATASOURCE"]).optional().describe("Connection type \u2014 DATASOURCE, DATAMART, or MULTI_DATASOURCE"),
|
|
94
|
-
datasourceName: z.string().optional().describe("Datasource name (required when connectionType is DATASOURCE)"),
|
|
95
|
-
datamartName: z.string().optional().describe("Datamart name (required when connectionType is DATAMART)"),
|
|
96
|
-
body: z.record(z.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/workspace")
|
|
97
|
-
},
|
|
98
|
-
WRITE_CREATE,
|
|
99
|
-
async (params) => {
|
|
100
|
-
try {
|
|
101
|
-
const body = params.body ? { ...params.body } : {
|
|
102
|
-
name: params.name,
|
|
103
|
-
connectionType: params.connectionType,
|
|
104
|
-
datasourceName: params.datasourceName,
|
|
105
|
-
datamartName: params.datamartName
|
|
106
|
-
};
|
|
107
|
-
const result = await client.request({
|
|
108
|
-
method: "POST",
|
|
109
|
-
path: "/api/v2/workspace",
|
|
110
|
-
body,
|
|
111
|
-
auth: "service",
|
|
112
|
-
invalidateCache: ["list_workspaces"]
|
|
113
|
-
});
|
|
114
|
-
if (result.error) {
|
|
115
|
-
return toolError(`Workspace creation failed: ${stringify(result.error)}`);
|
|
116
|
-
}
|
|
117
|
-
return toolSuccess(
|
|
118
|
-
`Workspace "${result.name || params.name}" created successfully.
|
|
119
|
-
|
|
120
|
-
Next steps:
|
|
121
|
-
1. Use \`create_data_app\` to create a data app linked to this workspace
|
|
122
|
-
2. Build dashboards in the Databrain UI using this workspace
|
|
123
|
-
3. Use \`create_embed\` to embed dashboards`
|
|
124
|
-
);
|
|
125
|
-
} catch (err) {
|
|
126
|
-
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
127
|
-
return toolError(`Unexpected error: ${err.message}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
103
|
// src/hosted/authorization.ts
|
|
134
104
|
var TOOL_SCOPES = {
|
|
135
105
|
whoami: [],
|
|
@@ -158,7 +128,11 @@ var TOOL_SCOPES = {
|
|
|
158
128
|
create_dashboard: ["mcp:dashboards:write"],
|
|
159
129
|
get_dashboard: ["mcp:dashboards:read"],
|
|
160
130
|
dashboard_snapshot: ["mcp:dashboards:read"],
|
|
161
|
-
generate_dashboard_tiles: [
|
|
131
|
+
generate_dashboard_tiles: [
|
|
132
|
+
"mcp:dashboards:write",
|
|
133
|
+
"mcp:query",
|
|
134
|
+
"mcp:embeds:write"
|
|
135
|
+
],
|
|
162
136
|
ask_question: ["mcp:query"],
|
|
163
137
|
get_semantic_layer: ["mcp:datamarts:read"],
|
|
164
138
|
update_semantic_layer: ["mcp:datamarts:write"],
|
|
@@ -170,8 +144,16 @@ var TOOL_SCOPES = {
|
|
|
170
144
|
apply_semantic_change: ["mcp:datamarts:write"],
|
|
171
145
|
eval_semantic_model: ["mcp:datamarts:read", "mcp:query"],
|
|
172
146
|
list_widgets: ["mcp:dashboards:read"],
|
|
173
|
-
validate_metric_spec: [
|
|
174
|
-
|
|
147
|
+
validate_metric_spec: [
|
|
148
|
+
"mcp:dashboards:read",
|
|
149
|
+
"mcp:query",
|
|
150
|
+
"mcp:embeds:write"
|
|
151
|
+
],
|
|
152
|
+
create_or_update_metric_from_query: [
|
|
153
|
+
"mcp:dashboards:write",
|
|
154
|
+
"mcp:query",
|
|
155
|
+
"mcp:embeds:write"
|
|
156
|
+
],
|
|
175
157
|
create_workspace_metric: ["mcp:dashboards:write", "mcp:query"],
|
|
176
158
|
add_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
|
|
177
159
|
update_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
|
|
@@ -199,12 +181,15 @@ var TOOL_SCOPES = {
|
|
|
199
181
|
change_date_range: ["mcp:dashboards:read", "mcp:query"]
|
|
200
182
|
};
|
|
201
183
|
function requiredScopes(name) {
|
|
202
|
-
if (!Object.hasOwn(TOOL_SCOPES, name))
|
|
184
|
+
if (!Object.hasOwn(TOOL_SCOPES, name))
|
|
185
|
+
throw new HostedAuthError(403, "tool_not_authorized");
|
|
203
186
|
return TOOL_SCOPES[name];
|
|
204
187
|
}
|
|
205
188
|
function authorizeTool(name, principal) {
|
|
206
|
-
if (principal.expiresAt !== void 0 && principal.expiresAt <= Date.now() / 1e3)
|
|
207
|
-
|
|
189
|
+
if (principal.expiresAt !== void 0 && principal.expiresAt <= Date.now() / 1e3)
|
|
190
|
+
throw new HostedAuthError(401, "invalid_token");
|
|
191
|
+
if (requiredScopes(name).some((scope) => !principal.scopes.includes(scope)))
|
|
192
|
+
throw new HostedAuthError(403, "insufficient_scope");
|
|
208
193
|
}
|
|
209
194
|
var TOOL_ARGUMENT_VALUES = {
|
|
210
195
|
list_dashboards: { scope: ["auto", "data_app", "global"] }
|
|
@@ -228,7 +213,8 @@ function assertToolArguments(name, value, principal, delegated = false) {
|
|
|
228
213
|
rest[key] = child;
|
|
229
214
|
continue;
|
|
230
215
|
}
|
|
231
|
-
if (typeof child !== "string" || !allowed[key].includes(child))
|
|
216
|
+
if (typeof child !== "string" || !allowed[key].includes(child))
|
|
217
|
+
throw new HostedAuthError(403, "identity_argument_forbidden");
|
|
232
218
|
}
|
|
233
219
|
assertBoundArguments(rest, principal, { target, delegated, opaque });
|
|
234
220
|
return target;
|
|
@@ -247,7 +233,9 @@ var WORKSPACE_FREE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
247
233
|
"list_custom_charts",
|
|
248
234
|
"publish_custom_chart"
|
|
249
235
|
]);
|
|
250
|
-
var WORKSPACE_SPANNING_TOOLS = /* @__PURE__ */ new Set([
|
|
236
|
+
var WORKSPACE_SPANNING_TOOLS = /* @__PURE__ */ new Set([
|
|
237
|
+
"list_dashboards"
|
|
238
|
+
]);
|
|
251
239
|
var WORKSPACE_INPUT = "workspace";
|
|
252
240
|
var WorkspaceSelectionError = class extends HostedAuthError {
|
|
253
241
|
constructor(status, code, reason) {
|
|
@@ -260,7 +248,9 @@ function grantedWorkspaces(principal) {
|
|
|
260
248
|
return principal.workspaceId ? [{ id: principal.workspaceId, name: principal.workspaceName }] : [];
|
|
261
249
|
}
|
|
262
250
|
function grantedNames(principal) {
|
|
263
|
-
const names = grantedWorkspaces(principal).map(
|
|
251
|
+
const names = grantedWorkspaces(principal).map(
|
|
252
|
+
(item) => item.name ?? item.id
|
|
253
|
+
);
|
|
264
254
|
return names.length > 20 ? `${names.slice(0, 20).join(", ")} and ${names.length - 20} more` : names.join(", ");
|
|
265
255
|
}
|
|
266
256
|
function findGranted(principal, key, value) {
|
|
@@ -297,7 +287,9 @@ function selectWorkspace(args, principal, keys = [WORKSPACE_INPUT, "workspaceNam
|
|
|
297
287
|
}
|
|
298
288
|
function namesWorkspace(value) {
|
|
299
289
|
if (!value || typeof value !== "object") return false;
|
|
300
|
-
return Object.entries(value).some(
|
|
290
|
+
return Object.entries(value).some(
|
|
291
|
+
([key, child]) => [WORKSPACE_INPUT, "workspaceName", "workspaceId"].includes(key) && child !== void 0 || namesWorkspace(child)
|
|
292
|
+
);
|
|
301
293
|
}
|
|
302
294
|
function tenantAllowed(child, principal, delegated) {
|
|
303
295
|
if (typeof child !== "string") return false;
|
|
@@ -343,18 +335,85 @@ function assertBoundArguments(value, principal, binding = {}) {
|
|
|
343
335
|
}
|
|
344
336
|
|
|
345
337
|
// src/hosted/server.ts
|
|
346
|
-
|
|
338
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
339
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
340
|
+
import { instrument } from "@posthog/mcp";
|
|
341
|
+
import { PostHog } from "posthog-node";
|
|
342
|
+
import { z as z2 } from "zod";
|
|
343
|
+
|
|
344
|
+
// src/tools/infrastructure/create-workspace.ts
|
|
345
|
+
import { z } from "zod";
|
|
346
|
+
function registerCreateWorkspace(server, client) {
|
|
347
|
+
server.tool(
|
|
348
|
+
"create_workspace",
|
|
349
|
+
'Create a new workspace to connect a datasource or datamart for dashboard building. Works only when this sign-in granted "All workspaces I can access" and your role may create workspaces; the new workspace is usable on the next call.',
|
|
350
|
+
{
|
|
351
|
+
name: z.string().describe("Unique workspace name"),
|
|
352
|
+
connectionType: z.enum(["DATASOURCE", "DATAMART", "MULTI_DATASOURCE"]).optional().describe(
|
|
353
|
+
"Connection type \u2014 DATASOURCE, DATAMART, or MULTI_DATASOURCE"
|
|
354
|
+
),
|
|
355
|
+
datasourceName: z.string().optional().describe(
|
|
356
|
+
"Datasource name (required when connectionType is DATASOURCE)"
|
|
357
|
+
),
|
|
358
|
+
datamartName: z.string().optional().describe("Datamart name (required when connectionType is DATAMART)"),
|
|
359
|
+
body: z.record(z.string(), z.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/workspace")
|
|
360
|
+
},
|
|
361
|
+
WRITE_CREATE,
|
|
362
|
+
async (params) => {
|
|
363
|
+
try {
|
|
364
|
+
const body = params.body ? { ...params.body } : {
|
|
365
|
+
name: params.name,
|
|
366
|
+
connectionType: params.connectionType,
|
|
367
|
+
datasourceName: params.datasourceName,
|
|
368
|
+
datamartName: params.datamartName
|
|
369
|
+
};
|
|
370
|
+
const result = await client.request({
|
|
371
|
+
method: "POST",
|
|
372
|
+
path: "/api/v2/workspace",
|
|
373
|
+
body,
|
|
374
|
+
auth: "service",
|
|
375
|
+
invalidateCache: ["list_workspaces"]
|
|
376
|
+
});
|
|
377
|
+
if (result.error) {
|
|
378
|
+
return toolError(
|
|
379
|
+
`Workspace creation failed: ${stringify(result.error)}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return toolSuccess(
|
|
383
|
+
`Workspace "${result.name || params.name}" created successfully.
|
|
384
|
+
|
|
385
|
+
Next steps:
|
|
386
|
+
1. Use \`create_data_app\` to create a data app linked to this workspace
|
|
387
|
+
2. Build dashboards in the Databrain UI using this workspace
|
|
388
|
+
3. Use \`create_embed\` to embed dashboards`
|
|
389
|
+
);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
392
|
+
return toolError(`Unexpected error: ${err.message}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/hosted/server.ts
|
|
399
|
+
var schemaInstrumentation = new PostHog("hosted-contract-only", {
|
|
400
|
+
disabled: true
|
|
401
|
+
});
|
|
347
402
|
var callWorkspace = new AsyncLocalStorage();
|
|
348
403
|
var WORKSPACE_HINT = "Call whoami to see the workspaces this sign-in granted, then name one.";
|
|
349
404
|
function workspaceError(error) {
|
|
350
|
-
return toolError(
|
|
405
|
+
return toolError(
|
|
406
|
+
`Error: ${error.reason} (${error.code})
|
|
351
407
|
|
|
352
|
-
Hint: ${WORKSPACE_HINT}`
|
|
408
|
+
Hint: ${WORKSPACE_HINT}`
|
|
409
|
+
);
|
|
353
410
|
}
|
|
354
411
|
function scopeError(missing) {
|
|
355
|
-
return toolError(
|
|
412
|
+
return toolError(
|
|
413
|
+
`Error: This connection cannot do this. It needs ${missing.join(", ")}, which your role did not allow when you connected. (INSUFFICIENT_SCOPE)
|
|
356
414
|
|
|
357
|
-
Hint: Ask a Databrain admin for a role that allows it, then connect again. If your role allows it in a workspace you did not choose, connect again and choose that workspace.`
|
|
415
|
+
Hint: Ask a Databrain admin for a role that allows it, then connect again. If your role allows it in a workspace you did not choose, connect again and choose that workspace.`
|
|
416
|
+
);
|
|
358
417
|
}
|
|
359
418
|
function onlyWorkspace(raw, target) {
|
|
360
419
|
const keep = (items) => Array.isArray(items) ? items.filter((item) => {
|
|
@@ -362,12 +421,16 @@ function onlyWorkspace(raw, target) {
|
|
|
362
421
|
return record.id === target.id || record._id === target.id || record.id === void 0 && record._id === void 0 && target.name !== void 0 && record.name === target.name;
|
|
363
422
|
}) : items;
|
|
364
423
|
if (Array.isArray(raw)) return keep(raw);
|
|
365
|
-
if (raw && typeof raw === "object" && "data" in raw)
|
|
424
|
+
if (raw && typeof raw === "object" && "data" in raw)
|
|
425
|
+
return { ...raw, data: keep(raw.data) };
|
|
366
426
|
return raw;
|
|
367
427
|
}
|
|
368
428
|
var HostedClient = class extends DatabrainClient {
|
|
369
429
|
constructor(adapter, principal, delegation) {
|
|
370
|
-
super({
|
|
430
|
+
super({
|
|
431
|
+
apiUrl: "https://api.usedatabrain.com",
|
|
432
|
+
demoDomain: "https://demo.usedatabrain.com"
|
|
433
|
+
});
|
|
371
434
|
this.adapter = adapter;
|
|
372
435
|
this.principal = principal;
|
|
373
436
|
this.delegation = delegation;
|
|
@@ -384,23 +447,46 @@ var HostedClient = class extends DatabrainClient {
|
|
|
384
447
|
if (!name) throw new HostedAuthError(403, "tool_not_authorized");
|
|
385
448
|
authorizeTool(name, this.principal);
|
|
386
449
|
const named = callWorkspace.getStore()?.target;
|
|
387
|
-
const target = named ??
|
|
450
|
+
const target = named ?? selectWorkspace(options.body, this.principal, [
|
|
451
|
+
"workspaceName",
|
|
452
|
+
"workspaceId"
|
|
453
|
+
]) ?? selectWorkspace(options.query, this.principal, [
|
|
454
|
+
"workspaceName",
|
|
455
|
+
"workspaceId"
|
|
456
|
+
]);
|
|
388
457
|
const delegated = this.delegation !== void 0;
|
|
389
|
-
const opaque = options.method === "POST" && options.path === PREVIEW_PATH ? ["rows"] : options.method === "POST" && /^\/api\/v2\/custom-charts\/[a-z][a-z0-9_-]*\/bryn\/turn$/.test(
|
|
390
|
-
|
|
391
|
-
|
|
458
|
+
const opaque = options.method === "POST" && options.path === PREVIEW_PATH ? ["rows"] : options.method === "POST" && /^\/api\/v2\/custom-charts\/[a-z][a-z0-9_-]*\/bryn\/turn$/.test(
|
|
459
|
+
options.path
|
|
460
|
+
) ? ["draft"] : void 0;
|
|
461
|
+
assertBoundArguments(options.body, this.principal, {
|
|
462
|
+
target,
|
|
463
|
+
delegated,
|
|
464
|
+
opaque
|
|
465
|
+
});
|
|
466
|
+
assertBoundArguments(options.query, this.principal, {
|
|
467
|
+
target,
|
|
468
|
+
delegated
|
|
469
|
+
});
|
|
392
470
|
let raw = await this.adapter.executeRequest(
|
|
393
471
|
structuredClone(options),
|
|
394
472
|
structuredClone(this.principal),
|
|
395
473
|
this.delegation,
|
|
396
474
|
target && structuredClone(target)
|
|
397
475
|
);
|
|
398
|
-
if (named && options.method === "GET" && options.path === "/api/v2/workspace")
|
|
399
|
-
|
|
476
|
+
if (named && options.method === "GET" && options.path === "/api/v2/workspace")
|
|
477
|
+
raw = onlyWorkspace(raw, named);
|
|
478
|
+
if (!options.skipDataUnwrap && raw && typeof raw === "object" && !Array.isArray(raw) && "data" in raw && raw.data !== null && raw.data !== void 0)
|
|
479
|
+
return raw.data;
|
|
400
480
|
return raw;
|
|
401
481
|
} catch (error) {
|
|
402
482
|
if (error instanceof WorkspaceSelectionError) {
|
|
403
|
-
throw new DatabrainApiError(
|
|
483
|
+
throw new DatabrainApiError(
|
|
484
|
+
error.status,
|
|
485
|
+
error.code,
|
|
486
|
+
`${error.reason} (${error.code})`,
|
|
487
|
+
void 0,
|
|
488
|
+
WORKSPACE_HINT
|
|
489
|
+
);
|
|
404
490
|
}
|
|
405
491
|
const status = error instanceof HostedAuthError ? error.status : 503;
|
|
406
492
|
const detail = error instanceof HostedAuthError ? error.detail : void 0;
|
|
@@ -442,37 +528,56 @@ function workspaceLine(name, input) {
|
|
|
442
528
|
}
|
|
443
529
|
function whoami(principal) {
|
|
444
530
|
const granted = grantedWorkspaces(principal);
|
|
445
|
-
return toolSuccess(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
531
|
+
return toolSuccess(
|
|
532
|
+
JSON.stringify(
|
|
533
|
+
{
|
|
534
|
+
status: "authenticated",
|
|
535
|
+
activeBearer: principal.credentialType,
|
|
536
|
+
actorKind: principal.actorKind,
|
|
537
|
+
userId: principal.userId,
|
|
538
|
+
companyId: principal.companyId,
|
|
539
|
+
workspaceGrant: principal.workspaces?.mode === "all" ? "All workspaces I can access (new ones are included)" : "Only the workspaces chosen at sign-in",
|
|
540
|
+
grantedWorkspaces: granted.map((item) => ({
|
|
541
|
+
id: item.id,
|
|
542
|
+
name: item.name
|
|
543
|
+
})),
|
|
544
|
+
...granted.length === 1 ? { workspaceId: granted[0].id, workspaceName: granted[0].name } : {},
|
|
545
|
+
note: granted.length === 1 ? "Every call works in this workspace." : `Name the workspace on each call that works in one (the \`${WORKSPACE_INPUT}\` or \`workspaceName\` input): ${grantedNames(principal)}.`,
|
|
546
|
+
oauthClientId: principal.clientId,
|
|
547
|
+
scopes: principal.scopes
|
|
548
|
+
},
|
|
549
|
+
null,
|
|
550
|
+
2
|
|
551
|
+
)
|
|
552
|
+
);
|
|
458
553
|
}
|
|
459
554
|
function createHostedToolServer(principal, adapter, delegation) {
|
|
460
555
|
const bound = structuredClone(principal);
|
|
461
556
|
const delegated = delegation !== void 0;
|
|
462
|
-
const server = new McpServer(
|
|
463
|
-
|
|
464
|
-
|
|
557
|
+
const server = new McpServer(
|
|
558
|
+
{ name: "databrain", version: MCP_SERVER_VERSION },
|
|
559
|
+
{
|
|
560
|
+
instructions: "Hosted Databrain: each request is authenticated as the signed-in user, with that user's own role in each workspace. Use whoami to see the verified identity, scopes and the workspaces this sign-in granted. When more than one workspace is granted, name the workspace on each call (the workspace or workspaceName input). Never ask for a service token or put credentials in tool arguments. Workspace and tenant access come from the bearer principal. Existing dry-run and production-confirmation rules still apply. For existing saved charts, call list_widgets with dashboardId or dashboardName and no clientId; no embed, data app or datamart is needed. Use render_metric_tile to draw a metricRef directly. Use ask_question for new data questions. Chart controls and save_chart_as_dashboard use the same workspace and role; saving requires the existing production confirmation. Hosts without MCP Apps can read the text answer."
|
|
561
|
+
}
|
|
562
|
+
);
|
|
465
563
|
const register = server.tool.bind(server);
|
|
466
564
|
server.tool = ((...args) => {
|
|
467
565
|
const name = args[0];
|
|
468
566
|
const index = args.length - 1;
|
|
469
567
|
const handler = args[index];
|
|
470
568
|
const shape = typeof args[1] === "string" && args[2] && typeof args[2] === "object" && !("readOnlyHint" in args[2]) ? args[2] : void 0;
|
|
471
|
-
const ownSelector = shape && ["workspaceName", "workspaceId"].find(
|
|
569
|
+
const ownSelector = shape && ["workspaceName", "workspaceId"].find(
|
|
570
|
+
(key) => Object.hasOwn(shape, key)
|
|
571
|
+
);
|
|
472
572
|
const addsInput = shape !== void 0 && !ownSelector && !WORKSPACE_FREE_TOOLS.has(name) && !Object.hasOwn(shape, WORKSPACE_INPUT);
|
|
473
573
|
if (shape && !WORKSPACE_FREE_TOOLS.has(name)) {
|
|
474
574
|
if (addsInput) {
|
|
475
|
-
args[2] = {
|
|
575
|
+
args[2] = {
|
|
576
|
+
...shape,
|
|
577
|
+
[WORKSPACE_INPUT]: z2.string().min(1).max(512).optional().describe(
|
|
578
|
+
WORKSPACE_SPANNING_TOOLS.has(name) ? "Workspace id or name. Leave it out to cover every granted workspace." : "Workspace id or name. Needed when you granted more than one workspace."
|
|
579
|
+
)
|
|
580
|
+
};
|
|
476
581
|
}
|
|
477
582
|
args[1] = `${args[1]}${workspaceLine(name, addsInput ? WORKSPACE_INPUT : ownSelector)}`;
|
|
478
583
|
}
|
|
@@ -482,7 +587,10 @@ function createHostedToolServer(principal, adapter, delegation) {
|
|
|
482
587
|
const target = assertToolArguments(name, values[0], bound, delegated);
|
|
483
588
|
if (name === "whoami") return whoami(bound);
|
|
484
589
|
const input = values[0] && typeof values[0] === "object" ? values[0] : void 0;
|
|
485
|
-
if (input?.projectDir !== void 0)
|
|
590
|
+
if (input?.projectDir !== void 0)
|
|
591
|
+
return toolError(
|
|
592
|
+
"projectDir is not available on the hosted server. Pass chartTs, manifest, bundleCss and iconSvg to compile in memory."
|
|
593
|
+
);
|
|
486
594
|
if (!target && !WORKSPACE_FREE_TOOLS.has(name) && !WORKSPACE_SPANNING_TOOLS.has(name) && !namesWorkspace(input)) {
|
|
487
595
|
throw new WorkspaceSelectionError(
|
|
488
596
|
400,
|
|
@@ -493,18 +601,29 @@ function createHostedToolServer(principal, adapter, delegation) {
|
|
|
493
601
|
if (input) {
|
|
494
602
|
if (addsInput) delete input[WORKSPACE_INPUT];
|
|
495
603
|
if (shape && target) {
|
|
496
|
-
if (Object.hasOwn(shape, "workspaceId"))
|
|
497
|
-
|
|
604
|
+
if (Object.hasOwn(shape, "workspaceId"))
|
|
605
|
+
input.workspaceId = target.id;
|
|
606
|
+
if (Object.hasOwn(shape, "workspaceName") && target.name !== void 0)
|
|
607
|
+
input.workspaceName = target.name;
|
|
498
608
|
}
|
|
499
609
|
}
|
|
500
|
-
const result = await callWorkspace.run(
|
|
610
|
+
const result = await callWorkspace.run(
|
|
611
|
+
{ target },
|
|
612
|
+
() => handler(...values)
|
|
613
|
+
);
|
|
501
614
|
if (target && result?._meta?.["ui/resourceUri"]) {
|
|
502
|
-
return {
|
|
615
|
+
return {
|
|
616
|
+
...result,
|
|
617
|
+
_meta: { ...result._meta, "databrain/workspace": target }
|
|
618
|
+
};
|
|
503
619
|
}
|
|
504
620
|
return result;
|
|
505
621
|
} catch (error) {
|
|
506
|
-
if (error instanceof WorkspaceSelectionError)
|
|
507
|
-
|
|
622
|
+
if (error instanceof WorkspaceSelectionError)
|
|
623
|
+
return workspaceError(error);
|
|
624
|
+
return toolError(
|
|
625
|
+
"Hosted authorization denied this tool call. Check the granted scopes, workspace and tenant access."
|
|
626
|
+
);
|
|
508
627
|
}
|
|
509
628
|
};
|
|
510
629
|
return register(...args);
|
|
@@ -527,7 +646,10 @@ function healthPath(url) {
|
|
|
527
646
|
return path === "/health";
|
|
528
647
|
}
|
|
529
648
|
function reply(res, status, body) {
|
|
530
|
-
res.writeHead(status, {
|
|
649
|
+
res.writeHead(status, {
|
|
650
|
+
"Content-Type": "application/json",
|
|
651
|
+
"Cache-Control": "no-store"
|
|
652
|
+
});
|
|
531
653
|
res.end(JSON.stringify(body));
|
|
532
654
|
}
|
|
533
655
|
async function readBody(req) {
|
|
@@ -565,7 +687,11 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
565
687
|
if (config.testOnly) res.setHeader("X-Databrain-Test-Only", "true");
|
|
566
688
|
try {
|
|
567
689
|
const localPort = server.address()?.port;
|
|
568
|
-
if (![
|
|
690
|
+
if (![
|
|
691
|
+
publicUrl.host,
|
|
692
|
+
`127.0.0.1:${localPort}`,
|
|
693
|
+
`localhost:${localPort}`
|
|
694
|
+
].includes(req.headers.host || "")) {
|
|
569
695
|
reply(res, 403, { error: "invalid_host" });
|
|
570
696
|
return;
|
|
571
697
|
}
|
|
@@ -576,10 +702,15 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
576
702
|
if (req.headers.origin) {
|
|
577
703
|
res.setHeader("Access-Control-Allow-Origin", publicUrl.origin);
|
|
578
704
|
res.setHeader("Vary", "Origin");
|
|
579
|
-
res.setHeader(
|
|
705
|
+
res.setHeader(
|
|
706
|
+
"Access-Control-Expose-Headers",
|
|
707
|
+
"WWW-Authenticate, MCP-Protocol-Version"
|
|
708
|
+
);
|
|
580
709
|
}
|
|
581
710
|
const url = new URL(req.url || "/", publicUrl.origin);
|
|
582
|
-
if (req.method === "GET" && [metadataPath, "/.well-known/oauth-protected-resource"].includes(
|
|
711
|
+
if (req.method === "GET" && [metadataPath, "/.well-known/oauth-protected-resource"].includes(
|
|
712
|
+
url.pathname
|
|
713
|
+
)) {
|
|
583
714
|
reply(res, 200, {
|
|
584
715
|
resource: config.publicUrl,
|
|
585
716
|
authorization_servers: [config.issuer],
|
|
@@ -598,15 +729,24 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
598
729
|
return;
|
|
599
730
|
}
|
|
600
731
|
if (req.method === "OPTIONS") {
|
|
601
|
-
res.setHeader(
|
|
602
|
-
|
|
732
|
+
res.setHeader(
|
|
733
|
+
"Access-Control-Allow-Methods",
|
|
734
|
+
"POST, GET, DELETE, OPTIONS"
|
|
735
|
+
);
|
|
736
|
+
res.setHeader(
|
|
737
|
+
"Access-Control-Allow-Headers",
|
|
738
|
+
"Authorization, Content-Type, MCP-Protocol-Version, MCP-Session-Id"
|
|
739
|
+
);
|
|
603
740
|
res.writeHead(204);
|
|
604
741
|
res.end();
|
|
605
742
|
return;
|
|
606
743
|
}
|
|
607
|
-
const authFields = req.rawHeaders.filter(
|
|
744
|
+
const authFields = req.rawHeaders.filter(
|
|
745
|
+
(_, i) => i % 2 === 0 && req.rawHeaders[i].toLowerCase() === "authorization"
|
|
746
|
+
);
|
|
608
747
|
const match = /^Bearer ([^\s]+)$/i.exec(req.headers.authorization || "");
|
|
609
|
-
if (authFields.length !== 1 || !match)
|
|
748
|
+
if (authFields.length !== 1 || !match)
|
|
749
|
+
throw new HostedAuthError(401, "invalid_token");
|
|
610
750
|
const principal = await verify(match[1]);
|
|
611
751
|
if (req.method !== "POST") {
|
|
612
752
|
res.setHeader("Allow", "POST, OPTIONS");
|
|
@@ -639,22 +779,40 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
639
779
|
if (body?.method === "tools/call" && typeof body.params?.name === "string") {
|
|
640
780
|
try {
|
|
641
781
|
authorizeTool(body.params.name, principal);
|
|
642
|
-
assertToolArguments(
|
|
782
|
+
assertToolArguments(
|
|
783
|
+
body.params.name,
|
|
784
|
+
body.params.arguments,
|
|
785
|
+
principal,
|
|
786
|
+
delegation !== void 0
|
|
787
|
+
);
|
|
643
788
|
} catch (error) {
|
|
644
789
|
if (error instanceof WorkspaceSelectionError) {
|
|
645
|
-
reply(res, 200, {
|
|
790
|
+
reply(res, 200, {
|
|
791
|
+
jsonrpc: "2.0",
|
|
792
|
+
id: body.id ?? null,
|
|
793
|
+
result: workspaceError(error)
|
|
794
|
+
});
|
|
646
795
|
return;
|
|
647
796
|
}
|
|
648
797
|
if (error instanceof HostedAuthError && error.code === "insufficient_scope") {
|
|
649
|
-
const missing = requiredScopes(body.params.name).filter(
|
|
650
|
-
|
|
798
|
+
const missing = requiredScopes(body.params.name).filter(
|
|
799
|
+
(scope) => !principal.scopes.includes(scope)
|
|
800
|
+
);
|
|
801
|
+
reply(res, 200, {
|
|
802
|
+
jsonrpc: "2.0",
|
|
803
|
+
id: body.id ?? null,
|
|
804
|
+
result: scopeError(missing)
|
|
805
|
+
});
|
|
651
806
|
return;
|
|
652
807
|
}
|
|
653
808
|
throw error;
|
|
654
809
|
}
|
|
655
810
|
}
|
|
656
811
|
const mcp = createHostedToolServer(principal, adapter, delegation);
|
|
657
|
-
const transport = new StreamableHTTPServerTransport({
|
|
812
|
+
const transport = new StreamableHTTPServerTransport({
|
|
813
|
+
sessionIdGenerator: void 0,
|
|
814
|
+
enableJsonResponse: true
|
|
815
|
+
});
|
|
658
816
|
let closed = false;
|
|
659
817
|
const close = () => {
|
|
660
818
|
if (!closed) {
|
|
@@ -678,8 +836,14 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
678
836
|
return;
|
|
679
837
|
}
|
|
680
838
|
const status = error instanceof HostedAuthError ? error.status : 503;
|
|
681
|
-
if (status === 401)
|
|
682
|
-
|
|
839
|
+
if (status === 401)
|
|
840
|
+
res.setHeader(
|
|
841
|
+
"WWW-Authenticate",
|
|
842
|
+
`Bearer error="invalid_token", resource_metadata="${metadataUrl}"`
|
|
843
|
+
);
|
|
844
|
+
reply(res, status, {
|
|
845
|
+
error: error instanceof HostedAuthError ? error.code : "temporarily_unavailable"
|
|
846
|
+
});
|
|
683
847
|
}
|
|
684
848
|
});
|
|
685
849
|
server.requestTimeout = 3e4;
|
|
@@ -694,7 +858,12 @@ var USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION} (hosted)`;
|
|
|
694
858
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
695
859
|
var FORWARDED_HEADERS = /* @__PURE__ */ new Set(["idempotency-key"]);
|
|
696
860
|
var WORKSPACE_HEADER = "X-Databrain-Workspace";
|
|
697
|
-
var GRANT_DENIALS = /* @__PURE__ */ new Set([
|
|
861
|
+
var GRANT_DENIALS = /* @__PURE__ */ new Set([
|
|
862
|
+
"ROLE_NOT_ALLOWED",
|
|
863
|
+
"WORKSPACE_CREATE_NOT_ALLOWED",
|
|
864
|
+
"forbidden_permission",
|
|
865
|
+
"insufficient_scope"
|
|
866
|
+
]);
|
|
698
867
|
function errorDetail(status, text, bearer) {
|
|
699
868
|
let parsed;
|
|
700
869
|
try {
|
|
@@ -734,7 +903,8 @@ function createProviderAdapter(config) {
|
|
|
734
903
|
if (issuer.protocol !== "https:" || issuer.username || issuer.password || issuer.search || issuer.hash) {
|
|
735
904
|
throw new Error("Invalid hosted authorization server configuration.");
|
|
736
905
|
}
|
|
737
|
-
const apiBase = config.issuer.replace(/\/$/, "");
|
|
906
|
+
const apiBase = (config.apiUrl ?? config.issuer).replace(/\/$/, "");
|
|
907
|
+
const apiOrigin = new URL(apiBase).origin;
|
|
738
908
|
const endpoint = `${apiBase}/oauth/principal`;
|
|
739
909
|
const publicOrigin = new URL(config.publicUrl).origin;
|
|
740
910
|
const bearers = /* @__PURE__ */ new WeakMap();
|
|
@@ -743,16 +913,23 @@ function createProviderAdapter(config) {
|
|
|
743
913
|
async verifyOAuth(token, resource) {
|
|
744
914
|
try {
|
|
745
915
|
const response = await fetch(endpoint, {
|
|
746
|
-
headers: {
|
|
916
|
+
headers: {
|
|
917
|
+
Authorization: `Bearer ${token}`,
|
|
918
|
+
Accept: "application/json"
|
|
919
|
+
},
|
|
747
920
|
redirect: "error",
|
|
748
921
|
signal: AbortSignal.timeout(1e4)
|
|
749
922
|
});
|
|
750
923
|
if (!response.ok) {
|
|
751
|
-
if (response.status === 429 || response.status >= 500)
|
|
924
|
+
if (response.status === 429 || response.status >= 500)
|
|
925
|
+
throw new HostedAuthError(503, "temporarily_unavailable");
|
|
752
926
|
throw new HostedAuthError(401, "invalid_token");
|
|
753
927
|
}
|
|
754
|
-
const { issuer: _issuer, ...principal } = responseSchema.parse(
|
|
755
|
-
|
|
928
|
+
const { issuer: _issuer, ...principal } = responseSchema.parse(
|
|
929
|
+
await response.json()
|
|
930
|
+
);
|
|
931
|
+
if (principal.resource !== resource || principal.credentialType !== "oauth")
|
|
932
|
+
throw new HostedAuthError(401, "invalid_token");
|
|
756
933
|
return principal;
|
|
757
934
|
} catch (error) {
|
|
758
935
|
if (error instanceof HostedAuthError) throw error;
|
|
@@ -763,18 +940,25 @@ function createProviderAdapter(config) {
|
|
|
763
940
|
throw new HostedAuthError(401, "pat_membership_adapter_required");
|
|
764
941
|
},
|
|
765
942
|
delegate(token) {
|
|
766
|
-
const handle = Object.freeze({
|
|
943
|
+
const handle = Object.freeze({
|
|
944
|
+
kind: "hosted-delegation"
|
|
945
|
+
});
|
|
767
946
|
bearers.set(handle, token);
|
|
768
947
|
return handle;
|
|
769
948
|
},
|
|
770
949
|
async executeRequest(options, principal, delegation, workspace) {
|
|
771
950
|
const bearer = delegation ? bearers.get(delegation) : void 0;
|
|
772
|
-
if (options.auth !== "none" && !bearer)
|
|
773
|
-
|
|
774
|
-
if (
|
|
951
|
+
if (options.auth !== "none" && !bearer)
|
|
952
|
+
throw new HostedAuthError(503, "delegated_backend_adapter_required");
|
|
953
|
+
if (options.auth === "none" && !options.token)
|
|
954
|
+
throw new HostedAuthError(403, "tool_not_authorized");
|
|
955
|
+
if (!options.path.startsWith("/") || options.path.startsWith("//"))
|
|
956
|
+
throw new HostedAuthError(403, "tool_not_authorized");
|
|
775
957
|
const url = new URL(`${apiBase}${options.path}`);
|
|
776
|
-
if (url.origin !==
|
|
777
|
-
|
|
958
|
+
if (url.origin !== apiOrigin)
|
|
959
|
+
throw new HostedAuthError(403, "tool_not_authorized");
|
|
960
|
+
for (const [key, value] of Object.entries(options.query ?? {}))
|
|
961
|
+
url.searchParams.set(key, value);
|
|
778
962
|
const headers = {
|
|
779
963
|
Accept: "application/json",
|
|
780
964
|
"Content-Type": "application/json",
|
|
@@ -797,7 +981,10 @@ function createProviderAdapter(config) {
|
|
|
797
981
|
} else {
|
|
798
982
|
headers.Authorization = `Bearer ${bearer}`;
|
|
799
983
|
if (workspace) {
|
|
800
|
-
if (!principal.grantedWorkspaces?.some(
|
|
984
|
+
if (!principal.grantedWorkspaces?.some(
|
|
985
|
+
(item) => item.id === workspace.id
|
|
986
|
+
) && principal.workspaceId !== workspace.id)
|
|
987
|
+
throw new HostedAuthError(403, "principal_mismatch");
|
|
801
988
|
headers[WORKSPACE_HEADER] = workspace.id;
|
|
802
989
|
}
|
|
803
990
|
}
|
|
@@ -815,7 +1002,10 @@ function createProviderAdapter(config) {
|
|
|
815
1002
|
throw new HostedAuthError(
|
|
816
1003
|
403,
|
|
817
1004
|
"delegated_request_denied",
|
|
818
|
-
forbidden.code === "insufficient_scope" && options.path === "/api/v2/guest-token/create" ? {
|
|
1005
|
+
forbidden.code === "insufficient_scope" && options.path === "/api/v2/guest-token/create" ? {
|
|
1006
|
+
...forbidden,
|
|
1007
|
+
message: "Guest/tenant operations require mcp:embeds:write. Ask an admin for a role that allows it, then connect again."
|
|
1008
|
+
} : forbidden
|
|
819
1009
|
);
|
|
820
1010
|
}
|
|
821
1011
|
const status = response.status === 403 && options.responseFormat === "studio-sse" ? 403 : response.status === 401 ? 401 : response.status >= 500 ? 503 : response.status === 403 || response.status === 404 ? 404 : 400;
|
|
@@ -837,33 +1027,51 @@ async function startHosted() {
|
|
|
837
1027
|
let adapter;
|
|
838
1028
|
if (config.testOnly) {
|
|
839
1029
|
const file = process.env.DATABRAIN_HOSTED_TEST_FIXTURES;
|
|
840
|
-
if (!file || !isAbsolute(file))
|
|
841
|
-
|
|
1030
|
+
if (!file || !isAbsolute(file))
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
"Hosted test mode requires an absolute fixture store path."
|
|
1033
|
+
);
|
|
1034
|
+
const { createTestOnlyAdapter } = await import("./test-only-6474NMTR.js");
|
|
842
1035
|
adapter = createTestOnlyAdapter(file);
|
|
843
1036
|
} else {
|
|
844
1037
|
const file = process.env.DATABRAIN_HOSTED_ADAPTER_MODULE;
|
|
845
1038
|
if (!file) {
|
|
846
1039
|
adapter = createProviderAdapter(config);
|
|
847
|
-
process.stderr.write(
|
|
1040
|
+
process.stderr.write(
|
|
1041
|
+
"[databrain] OAuth configured: data tools run as the signed-in user through the issuer; PAT membership requires a trusted adapter.\n"
|
|
1042
|
+
);
|
|
848
1043
|
} else {
|
|
849
|
-
if (!isAbsolute(file))
|
|
1044
|
+
if (!isAbsolute(file))
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
"DATABRAIN_HOSTED_ADAPTER_MODULE must be an absolute path."
|
|
1047
|
+
);
|
|
850
1048
|
try {
|
|
851
1049
|
adapter = (await import(pathToFileURL(file).href)).adapter;
|
|
852
1050
|
} catch {
|
|
853
1051
|
throw new Error("Could not load the trusted hosted adapter.");
|
|
854
1052
|
}
|
|
855
1053
|
}
|
|
856
|
-
if (!adapter || ["verifyOAuth", "resolvePatGrant", "executeRequest"].some(
|
|
1054
|
+
if (!adapter || ["verifyOAuth", "resolvePatGrant", "executeRequest"].some(
|
|
1055
|
+
(key) => typeof adapter[key] !== "function"
|
|
1056
|
+
))
|
|
1057
|
+
throw new Error("Invalid trusted hosted adapter.");
|
|
857
1058
|
}
|
|
858
1059
|
const patPath = process.env.DATABRAIN_HOSTED_PAT_STORE;
|
|
859
|
-
if (patPath && !isAbsolute(patPath))
|
|
860
|
-
|
|
1060
|
+
if (patPath && !isAbsolute(patPath))
|
|
1061
|
+
throw new Error("Hosted PAT store must be an absolute path.");
|
|
1062
|
+
const server = createHostedHttpServer(
|
|
1063
|
+
config,
|
|
1064
|
+
adapter,
|
|
1065
|
+
patPath ? new FilePatStore(patPath) : void 0
|
|
1066
|
+
);
|
|
861
1067
|
await new Promise((resolve, reject) => {
|
|
862
1068
|
server.once("error", reject);
|
|
863
1069
|
server.listen(config.port, config.bind, resolve);
|
|
864
1070
|
});
|
|
865
|
-
process.stderr.write(
|
|
866
|
-
`
|
|
1071
|
+
process.stderr.write(
|
|
1072
|
+
`[databrain] hosted HTTP ready on ${config.bind}:${config.port}/mcp${config.testOnly ? " \u2014 TEST ONLY: synthetic identity and data; no OAuth consent" : ""}
|
|
1073
|
+
`
|
|
1074
|
+
);
|
|
867
1075
|
const shutdown = () => {
|
|
868
1076
|
server.close(() => process.exit(0));
|
|
869
1077
|
setTimeout(() => {
|
|
@@ -877,4 +1085,4 @@ async function startHosted() {
|
|
|
877
1085
|
export {
|
|
878
1086
|
startHosted
|
|
879
1087
|
};
|
|
880
|
-
//# sourceMappingURL=main-
|
|
1088
|
+
//# sourceMappingURL=main-QCBVJSSR.js.map
|