@databrainhq/mcp-server 0.3.0-rc.13 → 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-AEQTEE5C.js → chunk-AWICHHDU.js} +153 -61
- package/dist/chunk-AWICHHDU.js.map +1 -0
- package/dist/{chunk-X44C5GW2.js → chunk-CEGHG5U3.js} +59 -18
- package/dist/chunk-CEGHG5U3.js.map +1 -0
- package/dist/{chunk-233VGFHY.js → chunk-NIC5MAGC.js} +317 -147
- package/dist/chunk-NIC5MAGC.js.map +1 -0
- package/dist/{chunk-DTFYHLYN.js → chunk-S65KA3Z7.js} +26 -11
- package/dist/chunk-S65KA3Z7.js.map +1 -0
- package/dist/{chunk-PSJVYD4W.js → chunk-UYFFFAYV.js} +11722 -10560
- package/dist/chunk-UYFFFAYV.js.map +1 -0
- package/dist/{chunk-HCYZN7I3.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-VEKNKWL5.js → main-QCBVJSSR.js} +348 -155
- package/dist/main-QCBVJSSR.js.map +1 -0
- package/dist/{test-only-LJGOY2QA.js → test-only-6474NMTR.js} +12 -8
- package/dist/test-only-6474NMTR.js.map +1 -0
- package/package.json +20 -9
- package/dist/chunk-233VGFHY.js.map +0 -1
- package/dist/chunk-AEQTEE5C.js.map +0 -1
- package/dist/chunk-DTFYHLYN.js.map +0 -1
- package/dist/chunk-HCYZN7I3.js.map +0 -1
- package/dist/chunk-PSJVYD4W.js.map +0 -1
- package/dist/chunk-Q5EYDKSA.js.map +0 -1
- package/dist/chunk-X44C5GW2.js.map +0 -1
- package/dist/main-VEKNKWL5.js.map +0 -1
- package/dist/test-only-LJGOY2QA.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,10 +63,12 @@ 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.");
|
|
65
72
|
let apiUrl;
|
|
66
73
|
if (env.DATABRAIN_HOSTED_API_URL) {
|
|
67
74
|
let url;
|
|
@@ -71,7 +78,9 @@ function loadHostedConfig(env = process.env) {
|
|
|
71
78
|
url = void 0;
|
|
72
79
|
}
|
|
73
80
|
if (!url || !["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
|
|
74
|
-
throw new Error(
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Configure DATABRAIN_HOSTED_API_URL as an origin without credentials, path, query or fragment."
|
|
83
|
+
);
|
|
75
84
|
}
|
|
76
85
|
apiUrl = url.origin;
|
|
77
86
|
}
|
|
@@ -86,64 +95,11 @@ function loadHostedConfig(env = process.env) {
|
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
// src/hosted/http.ts
|
|
89
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
createServer
|
|
100
|
+
} from "http";
|
|
90
101
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
91
102
|
|
|
92
|
-
// src/hosted/server.ts
|
|
93
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
94
|
-
import { z as z2 } from "zod";
|
|
95
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
96
|
-
import { instrument } from "@posthog/mcp";
|
|
97
|
-
import { PostHog } from "posthog-node";
|
|
98
|
-
|
|
99
|
-
// src/tools/infrastructure/create-workspace.ts
|
|
100
|
-
import { z } from "zod";
|
|
101
|
-
function registerCreateWorkspace(server, client) {
|
|
102
|
-
server.tool(
|
|
103
|
-
"create_workspace",
|
|
104
|
-
'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.',
|
|
105
|
-
{
|
|
106
|
-
name: z.string().describe("Unique workspace name"),
|
|
107
|
-
connectionType: z.enum(["DATASOURCE", "DATAMART", "MULTI_DATASOURCE"]).optional().describe("Connection type \u2014 DATASOURCE, DATAMART, or MULTI_DATASOURCE"),
|
|
108
|
-
datasourceName: z.string().optional().describe("Datasource name (required when connectionType is DATASOURCE)"),
|
|
109
|
-
datamartName: z.string().optional().describe("Datamart name (required when connectionType is DATAMART)"),
|
|
110
|
-
body: z.record(z.unknown()).optional().describe("Raw API body passthrough for POST /api/v2/workspace")
|
|
111
|
-
},
|
|
112
|
-
WRITE_CREATE,
|
|
113
|
-
async (params) => {
|
|
114
|
-
try {
|
|
115
|
-
const body = params.body ? { ...params.body } : {
|
|
116
|
-
name: params.name,
|
|
117
|
-
connectionType: params.connectionType,
|
|
118
|
-
datasourceName: params.datasourceName,
|
|
119
|
-
datamartName: params.datamartName
|
|
120
|
-
};
|
|
121
|
-
const result = await client.request({
|
|
122
|
-
method: "POST",
|
|
123
|
-
path: "/api/v2/workspace",
|
|
124
|
-
body,
|
|
125
|
-
auth: "service",
|
|
126
|
-
invalidateCache: ["list_workspaces"]
|
|
127
|
-
});
|
|
128
|
-
if (result.error) {
|
|
129
|
-
return toolError(`Workspace creation failed: ${stringify(result.error)}`);
|
|
130
|
-
}
|
|
131
|
-
return toolSuccess(
|
|
132
|
-
`Workspace "${result.name || params.name}" created successfully.
|
|
133
|
-
|
|
134
|
-
Next steps:
|
|
135
|
-
1. Use \`create_data_app\` to create a data app linked to this workspace
|
|
136
|
-
2. Build dashboards in the Databrain UI using this workspace
|
|
137
|
-
3. Use \`create_embed\` to embed dashboards`
|
|
138
|
-
);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
if (err instanceof DatabrainApiError) return err.toToolResponse();
|
|
141
|
-
return toolError(`Unexpected error: ${err.message}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
103
|
// src/hosted/authorization.ts
|
|
148
104
|
var TOOL_SCOPES = {
|
|
149
105
|
whoami: [],
|
|
@@ -172,7 +128,11 @@ var TOOL_SCOPES = {
|
|
|
172
128
|
create_dashboard: ["mcp:dashboards:write"],
|
|
173
129
|
get_dashboard: ["mcp:dashboards:read"],
|
|
174
130
|
dashboard_snapshot: ["mcp:dashboards:read"],
|
|
175
|
-
generate_dashboard_tiles: [
|
|
131
|
+
generate_dashboard_tiles: [
|
|
132
|
+
"mcp:dashboards:write",
|
|
133
|
+
"mcp:query",
|
|
134
|
+
"mcp:embeds:write"
|
|
135
|
+
],
|
|
176
136
|
ask_question: ["mcp:query"],
|
|
177
137
|
get_semantic_layer: ["mcp:datamarts:read"],
|
|
178
138
|
update_semantic_layer: ["mcp:datamarts:write"],
|
|
@@ -184,8 +144,16 @@ var TOOL_SCOPES = {
|
|
|
184
144
|
apply_semantic_change: ["mcp:datamarts:write"],
|
|
185
145
|
eval_semantic_model: ["mcp:datamarts:read", "mcp:query"],
|
|
186
146
|
list_widgets: ["mcp:dashboards:read"],
|
|
187
|
-
validate_metric_spec: [
|
|
188
|
-
|
|
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
|
+
],
|
|
189
157
|
create_workspace_metric: ["mcp:dashboards:write", "mcp:query"],
|
|
190
158
|
add_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
|
|
191
159
|
update_widget: ["mcp:dashboards:write", "mcp:embeds:write"],
|
|
@@ -213,12 +181,15 @@ var TOOL_SCOPES = {
|
|
|
213
181
|
change_date_range: ["mcp:dashboards:read", "mcp:query"]
|
|
214
182
|
};
|
|
215
183
|
function requiredScopes(name) {
|
|
216
|
-
if (!Object.hasOwn(TOOL_SCOPES, name))
|
|
184
|
+
if (!Object.hasOwn(TOOL_SCOPES, name))
|
|
185
|
+
throw new HostedAuthError(403, "tool_not_authorized");
|
|
217
186
|
return TOOL_SCOPES[name];
|
|
218
187
|
}
|
|
219
188
|
function authorizeTool(name, principal) {
|
|
220
|
-
if (principal.expiresAt !== void 0 && principal.expiresAt <= Date.now() / 1e3)
|
|
221
|
-
|
|
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");
|
|
222
193
|
}
|
|
223
194
|
var TOOL_ARGUMENT_VALUES = {
|
|
224
195
|
list_dashboards: { scope: ["auto", "data_app", "global"] }
|
|
@@ -242,7 +213,8 @@ function assertToolArguments(name, value, principal, delegated = false) {
|
|
|
242
213
|
rest[key] = child;
|
|
243
214
|
continue;
|
|
244
215
|
}
|
|
245
|
-
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");
|
|
246
218
|
}
|
|
247
219
|
assertBoundArguments(rest, principal, { target, delegated, opaque });
|
|
248
220
|
return target;
|
|
@@ -261,7 +233,9 @@ var WORKSPACE_FREE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
261
233
|
"list_custom_charts",
|
|
262
234
|
"publish_custom_chart"
|
|
263
235
|
]);
|
|
264
|
-
var WORKSPACE_SPANNING_TOOLS = /* @__PURE__ */ new Set([
|
|
236
|
+
var WORKSPACE_SPANNING_TOOLS = /* @__PURE__ */ new Set([
|
|
237
|
+
"list_dashboards"
|
|
238
|
+
]);
|
|
265
239
|
var WORKSPACE_INPUT = "workspace";
|
|
266
240
|
var WorkspaceSelectionError = class extends HostedAuthError {
|
|
267
241
|
constructor(status, code, reason) {
|
|
@@ -274,7 +248,9 @@ function grantedWorkspaces(principal) {
|
|
|
274
248
|
return principal.workspaceId ? [{ id: principal.workspaceId, name: principal.workspaceName }] : [];
|
|
275
249
|
}
|
|
276
250
|
function grantedNames(principal) {
|
|
277
|
-
const names = grantedWorkspaces(principal).map(
|
|
251
|
+
const names = grantedWorkspaces(principal).map(
|
|
252
|
+
(item) => item.name ?? item.id
|
|
253
|
+
);
|
|
278
254
|
return names.length > 20 ? `${names.slice(0, 20).join(", ")} and ${names.length - 20} more` : names.join(", ");
|
|
279
255
|
}
|
|
280
256
|
function findGranted(principal, key, value) {
|
|
@@ -311,7 +287,9 @@ function selectWorkspace(args, principal, keys = [WORKSPACE_INPUT, "workspaceNam
|
|
|
311
287
|
}
|
|
312
288
|
function namesWorkspace(value) {
|
|
313
289
|
if (!value || typeof value !== "object") return false;
|
|
314
|
-
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
|
+
);
|
|
315
293
|
}
|
|
316
294
|
function tenantAllowed(child, principal, delegated) {
|
|
317
295
|
if (typeof child !== "string") return false;
|
|
@@ -357,18 +335,85 @@ function assertBoundArguments(value, principal, binding = {}) {
|
|
|
357
335
|
}
|
|
358
336
|
|
|
359
337
|
// src/hosted/server.ts
|
|
360
|
-
|
|
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
|
+
});
|
|
361
402
|
var callWorkspace = new AsyncLocalStorage();
|
|
362
403
|
var WORKSPACE_HINT = "Call whoami to see the workspaces this sign-in granted, then name one.";
|
|
363
404
|
function workspaceError(error) {
|
|
364
|
-
return toolError(
|
|
405
|
+
return toolError(
|
|
406
|
+
`Error: ${error.reason} (${error.code})
|
|
365
407
|
|
|
366
|
-
Hint: ${WORKSPACE_HINT}`
|
|
408
|
+
Hint: ${WORKSPACE_HINT}`
|
|
409
|
+
);
|
|
367
410
|
}
|
|
368
411
|
function scopeError(missing) {
|
|
369
|
-
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)
|
|
370
414
|
|
|
371
|
-
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
|
+
);
|
|
372
417
|
}
|
|
373
418
|
function onlyWorkspace(raw, target) {
|
|
374
419
|
const keep = (items) => Array.isArray(items) ? items.filter((item) => {
|
|
@@ -376,12 +421,16 @@ function onlyWorkspace(raw, target) {
|
|
|
376
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;
|
|
377
422
|
}) : items;
|
|
378
423
|
if (Array.isArray(raw)) return keep(raw);
|
|
379
|
-
if (raw && typeof raw === "object" && "data" in raw)
|
|
424
|
+
if (raw && typeof raw === "object" && "data" in raw)
|
|
425
|
+
return { ...raw, data: keep(raw.data) };
|
|
380
426
|
return raw;
|
|
381
427
|
}
|
|
382
428
|
var HostedClient = class extends DatabrainClient {
|
|
383
429
|
constructor(adapter, principal, delegation) {
|
|
384
|
-
super({
|
|
430
|
+
super({
|
|
431
|
+
apiUrl: "https://api.usedatabrain.com",
|
|
432
|
+
demoDomain: "https://demo.usedatabrain.com"
|
|
433
|
+
});
|
|
385
434
|
this.adapter = adapter;
|
|
386
435
|
this.principal = principal;
|
|
387
436
|
this.delegation = delegation;
|
|
@@ -398,23 +447,46 @@ var HostedClient = class extends DatabrainClient {
|
|
|
398
447
|
if (!name) throw new HostedAuthError(403, "tool_not_authorized");
|
|
399
448
|
authorizeTool(name, this.principal);
|
|
400
449
|
const named = callWorkspace.getStore()?.target;
|
|
401
|
-
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
|
+
]);
|
|
402
457
|
const delegated = this.delegation !== void 0;
|
|
403
|
-
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(
|
|
404
|
-
|
|
405
|
-
|
|
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
|
+
});
|
|
406
470
|
let raw = await this.adapter.executeRequest(
|
|
407
471
|
structuredClone(options),
|
|
408
472
|
structuredClone(this.principal),
|
|
409
473
|
this.delegation,
|
|
410
474
|
target && structuredClone(target)
|
|
411
475
|
);
|
|
412
|
-
if (named && options.method === "GET" && options.path === "/api/v2/workspace")
|
|
413
|
-
|
|
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;
|
|
414
480
|
return raw;
|
|
415
481
|
} catch (error) {
|
|
416
482
|
if (error instanceof WorkspaceSelectionError) {
|
|
417
|
-
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
|
+
);
|
|
418
490
|
}
|
|
419
491
|
const status = error instanceof HostedAuthError ? error.status : 503;
|
|
420
492
|
const detail = error instanceof HostedAuthError ? error.detail : void 0;
|
|
@@ -456,37 +528,56 @@ function workspaceLine(name, input) {
|
|
|
456
528
|
}
|
|
457
529
|
function whoami(principal) {
|
|
458
530
|
const granted = grantedWorkspaces(principal);
|
|
459
|
-
return toolSuccess(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
+
);
|
|
472
553
|
}
|
|
473
554
|
function createHostedToolServer(principal, adapter, delegation) {
|
|
474
555
|
const bound = structuredClone(principal);
|
|
475
556
|
const delegated = delegation !== void 0;
|
|
476
|
-
const server = new McpServer(
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
);
|
|
479
563
|
const register = server.tool.bind(server);
|
|
480
564
|
server.tool = ((...args) => {
|
|
481
565
|
const name = args[0];
|
|
482
566
|
const index = args.length - 1;
|
|
483
567
|
const handler = args[index];
|
|
484
568
|
const shape = typeof args[1] === "string" && args[2] && typeof args[2] === "object" && !("readOnlyHint" in args[2]) ? args[2] : void 0;
|
|
485
|
-
const ownSelector = shape && ["workspaceName", "workspaceId"].find(
|
|
569
|
+
const ownSelector = shape && ["workspaceName", "workspaceId"].find(
|
|
570
|
+
(key) => Object.hasOwn(shape, key)
|
|
571
|
+
);
|
|
486
572
|
const addsInput = shape !== void 0 && !ownSelector && !WORKSPACE_FREE_TOOLS.has(name) && !Object.hasOwn(shape, WORKSPACE_INPUT);
|
|
487
573
|
if (shape && !WORKSPACE_FREE_TOOLS.has(name)) {
|
|
488
574
|
if (addsInput) {
|
|
489
|
-
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
|
+
};
|
|
490
581
|
}
|
|
491
582
|
args[1] = `${args[1]}${workspaceLine(name, addsInput ? WORKSPACE_INPUT : ownSelector)}`;
|
|
492
583
|
}
|
|
@@ -496,7 +587,10 @@ function createHostedToolServer(principal, adapter, delegation) {
|
|
|
496
587
|
const target = assertToolArguments(name, values[0], bound, delegated);
|
|
497
588
|
if (name === "whoami") return whoami(bound);
|
|
498
589
|
const input = values[0] && typeof values[0] === "object" ? values[0] : void 0;
|
|
499
|
-
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
|
+
);
|
|
500
594
|
if (!target && !WORKSPACE_FREE_TOOLS.has(name) && !WORKSPACE_SPANNING_TOOLS.has(name) && !namesWorkspace(input)) {
|
|
501
595
|
throw new WorkspaceSelectionError(
|
|
502
596
|
400,
|
|
@@ -507,18 +601,29 @@ function createHostedToolServer(principal, adapter, delegation) {
|
|
|
507
601
|
if (input) {
|
|
508
602
|
if (addsInput) delete input[WORKSPACE_INPUT];
|
|
509
603
|
if (shape && target) {
|
|
510
|
-
if (Object.hasOwn(shape, "workspaceId"))
|
|
511
|
-
|
|
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;
|
|
512
608
|
}
|
|
513
609
|
}
|
|
514
|
-
const result = await callWorkspace.run(
|
|
610
|
+
const result = await callWorkspace.run(
|
|
611
|
+
{ target },
|
|
612
|
+
() => handler(...values)
|
|
613
|
+
);
|
|
515
614
|
if (target && result?._meta?.["ui/resourceUri"]) {
|
|
516
|
-
return {
|
|
615
|
+
return {
|
|
616
|
+
...result,
|
|
617
|
+
_meta: { ...result._meta, "databrain/workspace": target }
|
|
618
|
+
};
|
|
517
619
|
}
|
|
518
620
|
return result;
|
|
519
621
|
} catch (error) {
|
|
520
|
-
if (error instanceof WorkspaceSelectionError)
|
|
521
|
-
|
|
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
|
+
);
|
|
522
627
|
}
|
|
523
628
|
};
|
|
524
629
|
return register(...args);
|
|
@@ -541,7 +646,10 @@ function healthPath(url) {
|
|
|
541
646
|
return path === "/health";
|
|
542
647
|
}
|
|
543
648
|
function reply(res, status, body) {
|
|
544
|
-
res.writeHead(status, {
|
|
649
|
+
res.writeHead(status, {
|
|
650
|
+
"Content-Type": "application/json",
|
|
651
|
+
"Cache-Control": "no-store"
|
|
652
|
+
});
|
|
545
653
|
res.end(JSON.stringify(body));
|
|
546
654
|
}
|
|
547
655
|
async function readBody(req) {
|
|
@@ -579,7 +687,11 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
579
687
|
if (config.testOnly) res.setHeader("X-Databrain-Test-Only", "true");
|
|
580
688
|
try {
|
|
581
689
|
const localPort = server.address()?.port;
|
|
582
|
-
if (![
|
|
690
|
+
if (![
|
|
691
|
+
publicUrl.host,
|
|
692
|
+
`127.0.0.1:${localPort}`,
|
|
693
|
+
`localhost:${localPort}`
|
|
694
|
+
].includes(req.headers.host || "")) {
|
|
583
695
|
reply(res, 403, { error: "invalid_host" });
|
|
584
696
|
return;
|
|
585
697
|
}
|
|
@@ -590,10 +702,15 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
590
702
|
if (req.headers.origin) {
|
|
591
703
|
res.setHeader("Access-Control-Allow-Origin", publicUrl.origin);
|
|
592
704
|
res.setHeader("Vary", "Origin");
|
|
593
|
-
res.setHeader(
|
|
705
|
+
res.setHeader(
|
|
706
|
+
"Access-Control-Expose-Headers",
|
|
707
|
+
"WWW-Authenticate, MCP-Protocol-Version"
|
|
708
|
+
);
|
|
594
709
|
}
|
|
595
710
|
const url = new URL(req.url || "/", publicUrl.origin);
|
|
596
|
-
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
|
+
)) {
|
|
597
714
|
reply(res, 200, {
|
|
598
715
|
resource: config.publicUrl,
|
|
599
716
|
authorization_servers: [config.issuer],
|
|
@@ -612,15 +729,24 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
612
729
|
return;
|
|
613
730
|
}
|
|
614
731
|
if (req.method === "OPTIONS") {
|
|
615
|
-
res.setHeader(
|
|
616
|
-
|
|
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
|
+
);
|
|
617
740
|
res.writeHead(204);
|
|
618
741
|
res.end();
|
|
619
742
|
return;
|
|
620
743
|
}
|
|
621
|
-
const authFields = req.rawHeaders.filter(
|
|
744
|
+
const authFields = req.rawHeaders.filter(
|
|
745
|
+
(_, i) => i % 2 === 0 && req.rawHeaders[i].toLowerCase() === "authorization"
|
|
746
|
+
);
|
|
622
747
|
const match = /^Bearer ([^\s]+)$/i.exec(req.headers.authorization || "");
|
|
623
|
-
if (authFields.length !== 1 || !match)
|
|
748
|
+
if (authFields.length !== 1 || !match)
|
|
749
|
+
throw new HostedAuthError(401, "invalid_token");
|
|
624
750
|
const principal = await verify(match[1]);
|
|
625
751
|
if (req.method !== "POST") {
|
|
626
752
|
res.setHeader("Allow", "POST, OPTIONS");
|
|
@@ -653,22 +779,40 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
653
779
|
if (body?.method === "tools/call" && typeof body.params?.name === "string") {
|
|
654
780
|
try {
|
|
655
781
|
authorizeTool(body.params.name, principal);
|
|
656
|
-
assertToolArguments(
|
|
782
|
+
assertToolArguments(
|
|
783
|
+
body.params.name,
|
|
784
|
+
body.params.arguments,
|
|
785
|
+
principal,
|
|
786
|
+
delegation !== void 0
|
|
787
|
+
);
|
|
657
788
|
} catch (error) {
|
|
658
789
|
if (error instanceof WorkspaceSelectionError) {
|
|
659
|
-
reply(res, 200, {
|
|
790
|
+
reply(res, 200, {
|
|
791
|
+
jsonrpc: "2.0",
|
|
792
|
+
id: body.id ?? null,
|
|
793
|
+
result: workspaceError(error)
|
|
794
|
+
});
|
|
660
795
|
return;
|
|
661
796
|
}
|
|
662
797
|
if (error instanceof HostedAuthError && error.code === "insufficient_scope") {
|
|
663
|
-
const missing = requiredScopes(body.params.name).filter(
|
|
664
|
-
|
|
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
|
+
});
|
|
665
806
|
return;
|
|
666
807
|
}
|
|
667
808
|
throw error;
|
|
668
809
|
}
|
|
669
810
|
}
|
|
670
811
|
const mcp = createHostedToolServer(principal, adapter, delegation);
|
|
671
|
-
const transport = new StreamableHTTPServerTransport({
|
|
812
|
+
const transport = new StreamableHTTPServerTransport({
|
|
813
|
+
sessionIdGenerator: void 0,
|
|
814
|
+
enableJsonResponse: true
|
|
815
|
+
});
|
|
672
816
|
let closed = false;
|
|
673
817
|
const close = () => {
|
|
674
818
|
if (!closed) {
|
|
@@ -692,8 +836,14 @@ function createHostedHttpServer(config, adapter, pats) {
|
|
|
692
836
|
return;
|
|
693
837
|
}
|
|
694
838
|
const status = error instanceof HostedAuthError ? error.status : 503;
|
|
695
|
-
if (status === 401)
|
|
696
|
-
|
|
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
|
+
});
|
|
697
847
|
}
|
|
698
848
|
});
|
|
699
849
|
server.requestTimeout = 3e4;
|
|
@@ -708,7 +858,12 @@ var USER_AGENT = `databrain-mcp-server/${MCP_SERVER_VERSION} (hosted)`;
|
|
|
708
858
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
709
859
|
var FORWARDED_HEADERS = /* @__PURE__ */ new Set(["idempotency-key"]);
|
|
710
860
|
var WORKSPACE_HEADER = "X-Databrain-Workspace";
|
|
711
|
-
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
|
+
]);
|
|
712
867
|
function errorDetail(status, text, bearer) {
|
|
713
868
|
let parsed;
|
|
714
869
|
try {
|
|
@@ -758,16 +913,23 @@ function createProviderAdapter(config) {
|
|
|
758
913
|
async verifyOAuth(token, resource) {
|
|
759
914
|
try {
|
|
760
915
|
const response = await fetch(endpoint, {
|
|
761
|
-
headers: {
|
|
916
|
+
headers: {
|
|
917
|
+
Authorization: `Bearer ${token}`,
|
|
918
|
+
Accept: "application/json"
|
|
919
|
+
},
|
|
762
920
|
redirect: "error",
|
|
763
921
|
signal: AbortSignal.timeout(1e4)
|
|
764
922
|
});
|
|
765
923
|
if (!response.ok) {
|
|
766
|
-
if (response.status === 429 || response.status >= 500)
|
|
924
|
+
if (response.status === 429 || response.status >= 500)
|
|
925
|
+
throw new HostedAuthError(503, "temporarily_unavailable");
|
|
767
926
|
throw new HostedAuthError(401, "invalid_token");
|
|
768
927
|
}
|
|
769
|
-
const { issuer: _issuer, ...principal } = responseSchema.parse(
|
|
770
|
-
|
|
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");
|
|
771
933
|
return principal;
|
|
772
934
|
} catch (error) {
|
|
773
935
|
if (error instanceof HostedAuthError) throw error;
|
|
@@ -778,18 +940,25 @@ function createProviderAdapter(config) {
|
|
|
778
940
|
throw new HostedAuthError(401, "pat_membership_adapter_required");
|
|
779
941
|
},
|
|
780
942
|
delegate(token) {
|
|
781
|
-
const handle = Object.freeze({
|
|
943
|
+
const handle = Object.freeze({
|
|
944
|
+
kind: "hosted-delegation"
|
|
945
|
+
});
|
|
782
946
|
bearers.set(handle, token);
|
|
783
947
|
return handle;
|
|
784
948
|
},
|
|
785
949
|
async executeRequest(options, principal, delegation, workspace) {
|
|
786
950
|
const bearer = delegation ? bearers.get(delegation) : void 0;
|
|
787
|
-
if (options.auth !== "none" && !bearer)
|
|
788
|
-
|
|
789
|
-
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");
|
|
790
957
|
const url = new URL(`${apiBase}${options.path}`);
|
|
791
|
-
if (url.origin !== apiOrigin)
|
|
792
|
-
|
|
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);
|
|
793
962
|
const headers = {
|
|
794
963
|
Accept: "application/json",
|
|
795
964
|
"Content-Type": "application/json",
|
|
@@ -812,7 +981,10 @@ function createProviderAdapter(config) {
|
|
|
812
981
|
} else {
|
|
813
982
|
headers.Authorization = `Bearer ${bearer}`;
|
|
814
983
|
if (workspace) {
|
|
815
|
-
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");
|
|
816
988
|
headers[WORKSPACE_HEADER] = workspace.id;
|
|
817
989
|
}
|
|
818
990
|
}
|
|
@@ -830,7 +1002,10 @@ function createProviderAdapter(config) {
|
|
|
830
1002
|
throw new HostedAuthError(
|
|
831
1003
|
403,
|
|
832
1004
|
"delegated_request_denied",
|
|
833
|
-
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
|
|
834
1009
|
);
|
|
835
1010
|
}
|
|
836
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;
|
|
@@ -852,33 +1027,51 @@ async function startHosted() {
|
|
|
852
1027
|
let adapter;
|
|
853
1028
|
if (config.testOnly) {
|
|
854
1029
|
const file = process.env.DATABRAIN_HOSTED_TEST_FIXTURES;
|
|
855
|
-
if (!file || !isAbsolute(file))
|
|
856
|
-
|
|
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");
|
|
857
1035
|
adapter = createTestOnlyAdapter(file);
|
|
858
1036
|
} else {
|
|
859
1037
|
const file = process.env.DATABRAIN_HOSTED_ADAPTER_MODULE;
|
|
860
1038
|
if (!file) {
|
|
861
1039
|
adapter = createProviderAdapter(config);
|
|
862
|
-
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
|
+
);
|
|
863
1043
|
} else {
|
|
864
|
-
if (!isAbsolute(file))
|
|
1044
|
+
if (!isAbsolute(file))
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
"DATABRAIN_HOSTED_ADAPTER_MODULE must be an absolute path."
|
|
1047
|
+
);
|
|
865
1048
|
try {
|
|
866
1049
|
adapter = (await import(pathToFileURL(file).href)).adapter;
|
|
867
1050
|
} catch {
|
|
868
1051
|
throw new Error("Could not load the trusted hosted adapter.");
|
|
869
1052
|
}
|
|
870
1053
|
}
|
|
871
|
-
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.");
|
|
872
1058
|
}
|
|
873
1059
|
const patPath = process.env.DATABRAIN_HOSTED_PAT_STORE;
|
|
874
|
-
if (patPath && !isAbsolute(patPath))
|
|
875
|
-
|
|
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
|
+
);
|
|
876
1067
|
await new Promise((resolve, reject) => {
|
|
877
1068
|
server.once("error", reject);
|
|
878
1069
|
server.listen(config.port, config.bind, resolve);
|
|
879
1070
|
});
|
|
880
|
-
process.stderr.write(
|
|
881
|
-
`
|
|
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
|
+
);
|
|
882
1075
|
const shutdown = () => {
|
|
883
1076
|
server.close(() => process.exit(0));
|
|
884
1077
|
setTimeout(() => {
|
|
@@ -892,4 +1085,4 @@ async function startHosted() {
|
|
|
892
1085
|
export {
|
|
893
1086
|
startHosted
|
|
894
1087
|
};
|
|
895
|
-
//# sourceMappingURL=main-
|
|
1088
|
+
//# sourceMappingURL=main-QCBVJSSR.js.map
|