@dobbyai/mcp-external 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -14
- package/dist/__tests__/agent-mail-tools.test.js +19 -26
- package/dist/__tests__/agent-mail-tools.test.js.map +1 -1
- package/dist/__tests__/auth.test.js +50 -5
- package/dist/__tests__/auth.test.js.map +1 -1
- package/dist/__tests__/doctor.test.d.ts +16 -0
- package/dist/__tests__/doctor.test.d.ts.map +1 -0
- package/dist/__tests__/doctor.test.js +258 -0
- package/dist/__tests__/doctor.test.js.map +1 -0
- package/dist/__tests__/external-agents-tools.test.d.ts +11 -0
- package/dist/__tests__/external-agents-tools.test.d.ts.map +1 -0
- package/dist/__tests__/external-agents-tools.test.js +619 -0
- package/dist/__tests__/external-agents-tools.test.js.map +1 -0
- package/dist/__tests__/gateway-bearer.test.d.ts +12 -0
- package/dist/__tests__/gateway-bearer.test.d.ts.map +1 -0
- package/dist/__tests__/gateway-bearer.test.js +112 -0
- package/dist/__tests__/gateway-bearer.test.js.map +1 -0
- package/dist/__tests__/gateway-key-resolver.test.d.ts +10 -0
- package/dist/__tests__/gateway-key-resolver.test.d.ts.map +1 -0
- package/dist/__tests__/gateway-key-resolver.test.js +125 -0
- package/dist/__tests__/gateway-key-resolver.test.js.map +1 -0
- package/dist/__tests__/http-concurrency.test.d.ts +20 -0
- package/dist/__tests__/http-concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/http-concurrency.test.js +254 -0
- package/dist/__tests__/http-concurrency.test.js.map +1 -0
- package/dist/__tests__/integration/protocol.test.js +14 -8
- package/dist/__tests__/integration/protocol.test.js.map +1 -1
- package/dist/__tests__/mcp-integrations.test.js +20 -30
- package/dist/__tests__/mcp-integrations.test.js.map +1 -1
- package/dist/__tests__/memory-graph-tools.test.js +27 -38
- package/dist/__tests__/memory-graph-tools.test.js.map +1 -1
- package/dist/__tests__/registry-dispatch.test.d.ts +11 -0
- package/dist/__tests__/registry-dispatch.test.d.ts.map +1 -0
- package/dist/__tests__/registry-dispatch.test.js +30 -0
- package/dist/__tests__/registry-dispatch.test.js.map +1 -0
- package/dist/__tests__/registry-listing-parity.test.d.ts +2 -0
- package/dist/__tests__/registry-listing-parity.test.d.ts.map +1 -0
- package/dist/__tests__/registry-listing-parity.test.js +89 -0
- package/dist/__tests__/registry-listing-parity.test.js.map +1 -0
- package/dist/__tests__/role-cap.test.d.ts +2 -0
- package/dist/__tests__/role-cap.test.d.ts.map +1 -0
- package/dist/__tests__/role-cap.test.js +113 -0
- package/dist/__tests__/role-cap.test.js.map +1 -0
- package/dist/__tests__/schedule-tools.test.js +15 -20
- package/dist/__tests__/schedule-tools.test.js.map +1 -1
- package/dist/__tests__/server-gateway-key.test.d.ts +18 -0
- package/dist/__tests__/server-gateway-key.test.d.ts.map +1 -0
- package/dist/__tests__/server-gateway-key.test.js +148 -0
- package/dist/__tests__/server-gateway-key.test.js.map +1 -0
- package/dist/__tests__/server-user-scoped.test.d.ts +34 -0
- package/dist/__tests__/server-user-scoped.test.d.ts.map +1 -0
- package/dist/__tests__/server-user-scoped.test.js +577 -0
- package/dist/__tests__/server-user-scoped.test.js.map +1 -0
- package/dist/__tests__/task-id-pattern.test.d.ts +12 -0
- package/dist/__tests__/task-id-pattern.test.d.ts.map +1 -0
- package/dist/__tests__/task-id-pattern.test.js +43 -0
- package/dist/__tests__/task-id-pattern.test.js.map +1 -0
- package/dist/__tests__/tiers.test.js +152 -56
- package/dist/__tests__/tiers.test.js.map +1 -1
- package/dist/__tests__/tools/hosted-agents.test.d.ts +9 -0
- package/dist/__tests__/tools/hosted-agents.test.d.ts.map +1 -0
- package/dist/__tests__/tools/hosted-agents.test.js +301 -0
- package/dist/__tests__/tools/hosted-agents.test.js.map +1 -0
- package/dist/__tests__/tools/llm-providers.test.d.ts +10 -0
- package/dist/__tests__/tools/llm-providers.test.d.ts.map +1 -0
- package/dist/__tests__/tools/llm-providers.test.js +105 -0
- package/dist/__tests__/tools/llm-providers.test.js.map +1 -0
- package/dist/__tests__/tools/services.test.js +139 -98
- package/dist/__tests__/tools/services.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +24 -15
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/__tests__/transport.test.js +365 -23
- package/dist/__tests__/transport.test.js.map +1 -1
- package/dist/__tests__/validate-credentials.test.d.ts +9 -0
- package/dist/__tests__/validate-credentials.test.d.ts.map +1 -0
- package/dist/__tests__/validate-credentials.test.js +172 -0
- package/dist/__tests__/validate-credentials.test.js.map +1 -0
- package/dist/__tests__/web-eval-tools.test.js +21 -32
- package/dist/__tests__/web-eval-tools.test.js.map +1 -1
- package/dist/__tests__/whoami.test.d.ts +11 -0
- package/dist/__tests__/whoami.test.d.ts.map +1 -0
- package/dist/__tests__/whoami.test.js +194 -0
- package/dist/__tests__/whoami.test.js.map +1 -0
- package/dist/__tests__/workflow-tools.test.js +20 -27
- package/dist/__tests__/workflow-tools.test.js.map +1 -1
- package/dist/__tests__/workloads.test.d.ts +9 -0
- package/dist/__tests__/workloads.test.d.ts.map +1 -0
- package/dist/__tests__/workloads.test.js +169 -0
- package/dist/__tests__/workloads.test.js.map +1 -0
- package/dist/auth/token-verifier.d.ts +11 -6
- package/dist/auth/token-verifier.d.ts.map +1 -1
- package/dist/auth/token-verifier.js +41 -19
- package/dist/auth/token-verifier.js.map +1 -1
- package/dist/connectors/claude-desktop.d.ts.map +1 -1
- package/dist/connectors/claude-desktop.js +3 -2
- package/dist/connectors/claude-desktop.js.map +1 -1
- package/dist/connectors/cursor.d.ts.map +1 -1
- package/dist/connectors/cursor.js +3 -2
- package/dist/connectors/cursor.js.map +1 -1
- package/dist/connectors/setup.d.ts.map +1 -1
- package/dist/connectors/setup.js +2 -1
- package/dist/connectors/setup.js.map +1 -1
- package/dist/connectors/vscode.d.ts.map +1 -1
- package/dist/connectors/vscode.js +3 -2
- package/dist/connectors/vscode.js.map +1 -1
- package/dist/connectors/windsurf.d.ts.map +1 -1
- package/dist/connectors/windsurf.js +3 -2
- package/dist/connectors/windsurf.js.map +1 -1
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +134 -0
- package/dist/doctor.js.map +1 -0
- package/dist/http-server.d.ts +24 -3
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +342 -62
- package/dist/http-server.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -208
- package/dist/index.js.map +1 -1
- package/dist/lib/consts.d.ts +21 -0
- package/dist/lib/consts.d.ts.map +1 -0
- package/dist/lib/consts.js +30 -0
- package/dist/lib/consts.js.map +1 -0
- package/dist/lib/dobby-client.d.ts +50 -23
- package/dist/lib/dobby-client.d.ts.map +1 -1
- package/dist/lib/dobby-client.js +105 -54
- package/dist/lib/dobby-client.js.map +1 -1
- package/dist/lib/gateway-key-resolver.d.ts +38 -0
- package/dist/lib/gateway-key-resolver.d.ts.map +1 -0
- package/dist/lib/gateway-key-resolver.js +59 -0
- package/dist/lib/gateway-key-resolver.js.map +1 -0
- package/dist/lib/log.d.ts +25 -0
- package/dist/lib/log.d.ts.map +1 -0
- package/dist/lib/log.js +36 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/server.d.ts +175 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1017 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/agent-mail.d.ts +0 -41
- package/dist/tools/agent-mail.d.ts.map +1 -1
- package/dist/tools/agent-mail.js +0 -222
- package/dist/tools/agent-mail.js.map +1 -1
- package/dist/tools/agent-versions.d.ts +93 -0
- package/dist/tools/agent-versions.d.ts.map +1 -0
- package/dist/tools/agent-versions.js +99 -0
- package/dist/tools/agent-versions.js.map +1 -0
- package/dist/tools/approvals.d.ts +0 -34
- package/dist/tools/approvals.d.ts.map +1 -1
- package/dist/tools/approvals.js +0 -81
- package/dist/tools/approvals.js.map +1 -1
- package/dist/tools/claude-code-sync.d.ts +40 -0
- package/dist/tools/claude-code-sync.d.ts.map +1 -0
- package/dist/tools/claude-code-sync.js +52 -0
- package/dist/tools/claude-code-sync.js.map +1 -0
- package/dist/tools/control-plane.d.ts +118 -0
- package/dist/tools/control-plane.d.ts.map +1 -0
- package/dist/tools/control-plane.js +135 -0
- package/dist/tools/control-plane.js.map +1 -0
- package/dist/tools/crew-builder.d.ts +0 -66
- package/dist/tools/crew-builder.d.ts.map +1 -1
- package/dist/tools/crew-builder.js +0 -185
- package/dist/tools/crew-builder.js.map +1 -1
- package/dist/tools/crewai.d.ts +0 -63
- package/dist/tools/crewai.d.ts.map +1 -1
- package/dist/tools/crewai.js +0 -145
- package/dist/tools/crewai.js.map +1 -1
- package/dist/tools/exa.d.ts +0 -35
- package/dist/tools/exa.d.ts.map +1 -1
- package/dist/tools/exa.js +0 -103
- package/dist/tools/exa.js.map +1 -1
- package/dist/tools/external-agents.d.ts +440 -0
- package/dist/tools/external-agents.d.ts.map +1 -0
- package/dist/tools/external-agents.js +386 -0
- package/dist/tools/external-agents.js.map +1 -0
- package/dist/tools/github-mcp.d.ts +0 -75
- package/dist/tools/github-mcp.d.ts.map +1 -1
- package/dist/tools/github-mcp.js +0 -162
- package/dist/tools/github-mcp.js.map +1 -1
- package/dist/tools/governance.d.ts +162 -18
- package/dist/tools/governance.d.ts.map +1 -1
- package/dist/tools/governance.js +208 -93
- package/dist/tools/governance.js.map +1 -1
- package/dist/tools/hosted-agents.d.ts +216 -0
- package/dist/tools/hosted-agents.d.ts.map +1 -0
- package/dist/tools/hosted-agents.js +214 -0
- package/dist/tools/hosted-agents.js.map +1 -0
- package/dist/tools/index.d.ts +3933 -1219
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +412 -393
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/jira.d.ts +0 -176
- package/dist/tools/jira.d.ts.map +1 -1
- package/dist/tools/jira.js +0 -365
- package/dist/tools/jira.js.map +1 -1
- package/dist/tools/llm-providers.d.ts +50 -0
- package/dist/tools/llm-providers.d.ts.map +1 -0
- package/dist/tools/llm-providers.js +41 -0
- package/dist/tools/llm-providers.js.map +1 -0
- package/dist/tools/mcp-metrics.d.ts +0 -15
- package/dist/tools/mcp-metrics.d.ts.map +1 -1
- package/dist/tools/mcp-metrics.js +0 -217
- package/dist/tools/mcp-metrics.js.map +1 -1
- package/dist/tools/mcp-tools.d.ts +0 -29
- package/dist/tools/mcp-tools.d.ts.map +1 -1
- package/dist/tools/mcp-tools.js +0 -131
- package/dist/tools/mcp-tools.js.map +1 -1
- package/dist/tools/mem0.d.ts +0 -41
- package/dist/tools/mem0.d.ts.map +1 -1
- package/dist/tools/mem0.js +65 -165
- package/dist/tools/mem0.js.map +1 -1
- package/dist/tools/memory-graph.d.ts +0 -46
- package/dist/tools/memory-graph.d.ts.map +1 -1
- package/dist/tools/memory-graph.js +0 -386
- package/dist/tools/memory-graph.js.map +1 -1
- package/dist/tools/memory-stores.d.ts +141 -0
- package/dist/tools/memory-stores.d.ts.map +1 -0
- package/dist/tools/memory-stores.js +147 -0
- package/dist/tools/memory-stores.js.map +1 -0
- package/dist/tools/messages.d.ts +0 -20
- package/dist/tools/messages.d.ts.map +1 -1
- package/dist/tools/messages.js +0 -49
- package/dist/tools/messages.js.map +1 -1
- package/dist/tools/moltbook.d.ts +0 -236
- package/dist/tools/moltbook.d.ts.map +1 -1
- package/dist/tools/moltbook.js +0 -458
- package/dist/tools/moltbook.js.map +1 -1
- package/dist/tools/monitoring-attribution.d.ts +71 -0
- package/dist/tools/monitoring-attribution.d.ts.map +1 -0
- package/dist/tools/monitoring-attribution.js +74 -0
- package/dist/tools/monitoring-attribution.js.map +1 -0
- package/dist/tools/ops.d.ts +0 -80
- package/dist/tools/ops.d.ts.map +1 -1
- package/dist/tools/ops.js +0 -209
- package/dist/tools/ops.js.map +1 -1
- package/dist/tools/orchestrations.d.ts +109 -0
- package/dist/tools/orchestrations.d.ts.map +1 -0
- package/dist/tools/orchestrations.js +115 -0
- package/dist/tools/orchestrations.js.map +1 -0
- package/dist/tools/outcomes.d.ts +138 -0
- package/dist/tools/outcomes.d.ts.map +1 -0
- package/dist/tools/outcomes.js +136 -0
- package/dist/tools/outcomes.js.map +1 -0
- package/dist/tools/schedules.d.ts +0 -47
- package/dist/tools/schedules.d.ts.map +1 -1
- package/dist/tools/schedules.js +0 -109
- package/dist/tools/schedules.js.map +1 -1
- package/dist/tools/services.d.ts +0 -16
- package/dist/tools/services.d.ts.map +1 -1
- package/dist/tools/services.js +0 -135
- package/dist/tools/services.js.map +1 -1
- package/dist/tools/sessions.d.ts +124 -0
- package/dist/tools/sessions.d.ts.map +1 -0
- package/dist/tools/sessions.js +130 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/tools/setup-status.d.ts +29 -0
- package/dist/tools/setup-status.d.ts.map +1 -0
- package/dist/tools/setup-status.js +35 -0
- package/dist/tools/setup-status.js.map +1 -0
- package/dist/tools/tasks.d.ts +0 -57
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +8 -217
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/tiers.d.ts +23 -7
- package/dist/tools/tiers.d.ts.map +1 -1
- package/dist/tools/tiers.js +180 -79
- package/dist/tools/tiers.js.map +1 -1
- package/dist/tools/unified-agents.d.ts +0 -19
- package/dist/tools/unified-agents.d.ts.map +1 -1
- package/dist/tools/unified-agents.js +2 -103
- package/dist/tools/unified-agents.js.map +1 -1
- package/dist/tools/web-eval.d.ts +0 -43
- package/dist/tools/web-eval.d.ts.map +1 -1
- package/dist/tools/web-eval.js +0 -211
- package/dist/tools/web-eval.js.map +1 -1
- package/dist/tools/workflow.d.ts +0 -66
- package/dist/tools/workflow.d.ts.map +1 -1
- package/dist/tools/workflow.js +2 -409
- package/dist/tools/workflow.js.map +1 -1
- package/dist/tools/workloads.d.ts +81 -0
- package/dist/tools/workloads.d.ts.map +1 -0
- package/dist/tools/workloads.js +81 -0
- package/dist/tools/workloads.js.map +1 -0
- package/package.json +7 -6
- package/dist/tools/knowledge-base.d.ts +0 -144
- package/dist/tools/knowledge-base.d.ts.map +0 -1
- package/dist/tools/knowledge-base.js +0 -179
- package/dist/tools/knowledge-base.js.map +0 -1
package/dist/server.js
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server factory
|
|
3
|
+
*
|
|
4
|
+
* Builds the Dobby MCP Server with all request handlers registered.
|
|
5
|
+
* Extracted from index.ts (2026-07-12) so tests can exercise the REAL
|
|
6
|
+
* handlers — index.ts runs main() on import, which is unsafe under vitest.
|
|
7
|
+
*
|
|
8
|
+
* In STDIO mode: tenant context comes from env/CLI config; full tool access
|
|
9
|
+
* (the local key already proves the tenant).
|
|
10
|
+
* In HTTP mode: tenant context comes from the OAuth token; tool access is
|
|
11
|
+
* ENFORCED per token from authInfo.scopes — the introspection endpoint
|
|
12
|
+
* (web/app/oauth/token/introspect) derives the effective mcp scope from the
|
|
13
|
+
* org's plan (tier) + the token's own scopes (read vs execute).
|
|
14
|
+
*/
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import { createDobbyClient, DobbyClient } from "./lib/dobby-client.js";
|
|
18
|
+
import { resolveSessionGatewayKey } from "./lib/gateway-key-resolver.js";
|
|
19
|
+
import { getToolDefinitions, getToolAccessDenial, handleToolCall, resolveToolTier, isReadOnlyScopes, } from "./tools/index.js";
|
|
20
|
+
import { resourceDefinitions, resourceTemplates, handleResourceRead, } from "./resources/index.js";
|
|
21
|
+
import { promptDefinitions, handlePromptGet } from "./prompts/index.js";
|
|
22
|
+
import { classifyKeyType } from "./lib/consts.js";
|
|
23
|
+
/**
|
|
24
|
+
* Synthetic discovery tool for a user-scoped session (ADR-020). It is NOT a
|
|
25
|
+
* registered capability (keeps the registry at 131 + all golden/parity/tier
|
|
26
|
+
* tests byte-identical) — the server injects it into ListTools and handles it
|
|
27
|
+
* in CallTool only when the session is user-scoped, because it is meaningless
|
|
28
|
+
* for a tenant-pinned connection (you already know your one tenant).
|
|
29
|
+
*/
|
|
30
|
+
const USER_SCOPED_TENANTS_TOOL = "list_my_tenants";
|
|
31
|
+
const USER_SCOPED_TENANTS_TOOL_DEF = {
|
|
32
|
+
name: USER_SCOPED_TENANTS_TOOL,
|
|
33
|
+
description: "List the tenants (workspaces) and organizations you can act in, with your " +
|
|
34
|
+
"live role in each. Use a returned tenant_id as the `tenant_id` argument to " +
|
|
35
|
+
"other tools in this user-scoped connection.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {},
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
},
|
|
41
|
+
annotations: {
|
|
42
|
+
title: "List my tenants",
|
|
43
|
+
readOnlyHint: true,
|
|
44
|
+
destructiveHint: false,
|
|
45
|
+
openWorldHint: false,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Structured "needs_selection" envelope — a user-scoped call omitted the scope
|
|
50
|
+
* argument it needs. Returned as machine-readable JSON (a client can JSON.parse
|
|
51
|
+
* `content[0].text` and read `status`/`argument`/`list_tool`) that is ALSO
|
|
52
|
+
* human-readable via the `message` field. isError stays true: the call could not
|
|
53
|
+
* proceed. UAT onboarding: return needs_selection, never silently inherit scope.
|
|
54
|
+
*/
|
|
55
|
+
export function needsSelectionResult(select, argument, listTool) {
|
|
56
|
+
const article = select === "organization" ? "an" : "a";
|
|
57
|
+
const envelope = {
|
|
58
|
+
status: "needs_selection",
|
|
59
|
+
select,
|
|
60
|
+
argument,
|
|
61
|
+
list_tool: listTool,
|
|
62
|
+
message: `This is a user-scoped connection with no fixed ${select}. Call ` +
|
|
63
|
+
`"${listTool}" to see the ${select}s you can access, then re-call this ` +
|
|
64
|
+
`tool with ${article} "${argument}" argument naming the ${select} to act in.`,
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{ type: "text", text: JSON.stringify(envelope, null, 2) },
|
|
69
|
+
],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Structured "needs_pat" envelope — an identity/discovery call needs a Personal
|
|
75
|
+
* Access Token that is absent or not accepted. Names the env var to set and what
|
|
76
|
+
* it unlocks, as machine-readable JSON with a human `message` (UAT onboarding).
|
|
77
|
+
*/
|
|
78
|
+
export function needsPatResult(unlocks, detail) {
|
|
79
|
+
const envelope = {
|
|
80
|
+
status: "needs_pat",
|
|
81
|
+
token_prefix: "dobby_pat_",
|
|
82
|
+
env_var: "DOBBY_API_KEY",
|
|
83
|
+
unlocks,
|
|
84
|
+
message: `A Personal Access Token (dobby_pat_…) is required to ${unlocks}. ` +
|
|
85
|
+
`Provide it as the DOBBY_API_KEY env var (or connect via OAuth to ` +
|
|
86
|
+
`mcp.dobby-ai.com), then retry. Create one under Settings → Personal ` +
|
|
87
|
+
`Access Tokens.` +
|
|
88
|
+
(detail ? ` (server said: ${detail})` : ""),
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{ type: "text", text: JSON.stringify(envelope, null, 2) },
|
|
93
|
+
],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Handle the synthetic `list_my_tenants` tool. Calls the web user-scoped
|
|
99
|
+
* endpoint (PAT-authed) that returns the caller's tenants + orgs + live role in
|
|
100
|
+
* each, so a user-scoped client can populate the `tenant_id` argument.
|
|
101
|
+
*/
|
|
102
|
+
async function handleListMyTenants(apiUrl, token) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(`${apiUrl}/api/v1/me/tenants`, {
|
|
105
|
+
method: "GET",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"X-API-Key": token,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const body = await response.text();
|
|
113
|
+
if (response.status === 401) {
|
|
114
|
+
return needsPatResult("list the tenants you can act in", body.slice(0, 200));
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: `Failed to list your tenants (${response.status}): ${body}`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const data = (await response.json());
|
|
127
|
+
const tenants = data.tenants ?? [];
|
|
128
|
+
if (tenants.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: "You are not a member of any tenants.",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
isError: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const lines = tenants.map((t) => `• ${t.name || t.tenant_id} — tenant_id: ${t.tenant_id}` +
|
|
140
|
+
(t.role ? ` · role: ${t.role}` : "") +
|
|
141
|
+
(t.region ? ` · region: ${t.region}` : "") +
|
|
142
|
+
(t.organization_id ? ` · org: ${t.organization_id}` : ""));
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `You can act in ${tenants.length} tenant(s). Pass the tenant_id of ` +
|
|
148
|
+
`the one you want when calling a tool:\n\n${lines.join("\n")}`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
isError: false,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: `Failed to list your tenants: ${error.message}`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
isError: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Short-prompt onboarding: resolve the caller's SOLE tenant. Returns the
|
|
168
|
+
* tenant_id when the user belongs to exactly ONE tenant — so a user-scoped call
|
|
169
|
+
* that omitted `tenant_id` can proceed silently without a needs_selection
|
|
170
|
+
* round-trip — or null when there are zero, many, or the lookup fails (the
|
|
171
|
+
* caller then falls back to the structured needs_selection envelope). Reuses the
|
|
172
|
+
* same PAT-authed /api/v1/me/tenants endpoint as list_my_tenants.
|
|
173
|
+
*/
|
|
174
|
+
export async function resolveSoleTenant(apiUrl, token) {
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch(`${apiUrl}/api/v1/me/tenants`, {
|
|
177
|
+
method: "GET",
|
|
178
|
+
headers: { "Content-Type": "application/json", "X-API-Key": token },
|
|
179
|
+
});
|
|
180
|
+
if (!response.ok)
|
|
181
|
+
return null;
|
|
182
|
+
const data = (await response.json());
|
|
183
|
+
const tenants = data.tenants ?? [];
|
|
184
|
+
return tenants.length === 1 ? tenants[0].tenant_id : null;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Synthetic discovery tool for the ORG side of a user-scoped session (ADR-020),
|
|
192
|
+
* the sibling of `list_my_tenants`. Also NOT a registered capability (keeps the
|
|
193
|
+
* registry at 131 + all golden/parity/tier tests byte-identical) — injected
|
|
194
|
+
* into ListTools and handled in CallTool only for a user-scoped session.
|
|
195
|
+
*/
|
|
196
|
+
const USER_SCOPED_ORGS_TOOL = "list_my_orgs";
|
|
197
|
+
const USER_SCOPED_ORGS_TOOL_DEF = {
|
|
198
|
+
name: USER_SCOPED_ORGS_TOOL,
|
|
199
|
+
description: "List the organizations you can act in, with your live role in each. Use a " +
|
|
200
|
+
"returned organization_id as the `org_id` argument to org-scoped tools " +
|
|
201
|
+
"(memory stores, rubrics, orchestration configs) in this user-scoped connection.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {},
|
|
205
|
+
additionalProperties: false,
|
|
206
|
+
},
|
|
207
|
+
annotations: {
|
|
208
|
+
title: "List my organizations",
|
|
209
|
+
readOnlyHint: true,
|
|
210
|
+
destructiveHint: false,
|
|
211
|
+
openWorldHint: false,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Handle the synthetic `list_my_orgs` tool. Calls the web user-scoped endpoint
|
|
216
|
+
* (PAT-authed) that returns the caller's orgs + live role in each, so a
|
|
217
|
+
* user-scoped client can populate the `org_id` argument.
|
|
218
|
+
*/
|
|
219
|
+
async function handleListMyOrgs(apiUrl, token) {
|
|
220
|
+
try {
|
|
221
|
+
const response = await fetch(`${apiUrl}/api/v1/me/organizations`, {
|
|
222
|
+
method: "GET",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
"X-API-Key": token,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
const body = await response.text();
|
|
230
|
+
if (response.status === 401) {
|
|
231
|
+
return needsPatResult("list the organizations you can act in", body.slice(0, 200));
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: `Failed to list your organizations (${response.status}): ${body}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
isError: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const data = (await response.json());
|
|
244
|
+
const orgs = data.organizations ?? [];
|
|
245
|
+
if (orgs.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: "You are not a member of any organizations.",
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
isError: false,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const lines = orgs.map((o) => `• ${o.name || o.organization_id} — org_id: ${o.organization_id}` +
|
|
257
|
+
(o.role ? ` · role: ${o.role}` : ""));
|
|
258
|
+
return {
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: `You can act in ${orgs.length} organization(s). Pass the org_id of ` +
|
|
263
|
+
`the one you want as the "org_id" argument:\n\n${lines.join("\n")}`,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
isError: false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
type: "text",
|
|
274
|
+
text: `Failed to list your organizations: ${error.message}`,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
isError: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Synthetic `whoami` tool (onboarding identity echo). Unlike list_my_tenants /
|
|
283
|
+
* list_my_orgs it is available in BOTH transports and is NEVER gated or errored
|
|
284
|
+
* — its whole job is to answer "who am I, what can this key do, is a PAT present,
|
|
285
|
+
* which environment am I on?" so an onboarding agent never has to read `ps aux`
|
|
286
|
+
* or guess which org/tenant it silently inherited. NOT a registered capability
|
|
287
|
+
* (keeps the registry at 131 + all golden/parity/tier tests byte-identical).
|
|
288
|
+
*/
|
|
289
|
+
const WHOAMI_TOOL = "whoami";
|
|
290
|
+
export const WHOAMI_TOOL_DEF = {
|
|
291
|
+
name: WHOAMI_TOOL,
|
|
292
|
+
description: "Report the current connection identity and what your credentials can do: " +
|
|
293
|
+
"transport, API URL, key type, the tenant/organization/user in context, whether " +
|
|
294
|
+
"a Personal Access Token is present, and your scopes. Never fails on missing " +
|
|
295
|
+
"credentials — it reports exactly what is set and what is missing. Call this " +
|
|
296
|
+
"FIRST to confirm which environment (prod vs dev) and which scope you are acting in.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {},
|
|
300
|
+
additionalProperties: false,
|
|
301
|
+
},
|
|
302
|
+
annotations: {
|
|
303
|
+
title: "Who am I",
|
|
304
|
+
readOnlyHint: true,
|
|
305
|
+
destructiveHint: false,
|
|
306
|
+
openWorldHint: false,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Build the identity echo for `whoami`. Never throws and never returns an error
|
|
311
|
+
* envelope — missing credentials are reported as `missing[]` notes, not failures.
|
|
312
|
+
*/
|
|
313
|
+
export function handleWhoami(input) {
|
|
314
|
+
const missing = [];
|
|
315
|
+
let identity;
|
|
316
|
+
if (input.mode === "stdio") {
|
|
317
|
+
const keyType = classifyKeyType(input.apiKey);
|
|
318
|
+
if (keyType === "none")
|
|
319
|
+
missing.push("DOBBY_API_KEY (no key configured)");
|
|
320
|
+
if (!input.tenantId)
|
|
321
|
+
missing.push("DOBBY_TENANT_ID (no default workspace set)");
|
|
322
|
+
if (keyType === "pat") {
|
|
323
|
+
missing.push("gateway/execute tools disabled — a dobby_pat_ cannot mint a gateway key in local STDIO; use a gk_ key, or connect over the remote HTTP transport (mcp.dobby-ai.com)");
|
|
324
|
+
}
|
|
325
|
+
else if (!input.gatewayEnabled) {
|
|
326
|
+
missing.push("DOBBY_GATEWAY_KEY (gateway/execute tools disabled; a gk_ DOBBY_API_KEY also enables them)");
|
|
327
|
+
}
|
|
328
|
+
identity = {
|
|
329
|
+
transport: "stdio",
|
|
330
|
+
api_url: input.apiUrl,
|
|
331
|
+
key_type: keyType,
|
|
332
|
+
pat_present: keyType === "pat",
|
|
333
|
+
gateway_tools_enabled: input.gatewayEnabled,
|
|
334
|
+
tenant_id: input.tenantId ?? null,
|
|
335
|
+
organization_id: input.organizationId ?? null,
|
|
336
|
+
user_id: input.userId,
|
|
337
|
+
user_scoped: false,
|
|
338
|
+
// Local STDIO is full-access; scopes are not enforced by the server.
|
|
339
|
+
scopes: null,
|
|
340
|
+
is_super_admin: null,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const token = input.authInfo?.token;
|
|
345
|
+
const extra = input.authInfo?.extra;
|
|
346
|
+
const userScoped = extra?.user_scoped === true;
|
|
347
|
+
const keyType = classifyKeyType(token);
|
|
348
|
+
if (!token)
|
|
349
|
+
missing.push("bearer token (no authenticated session)");
|
|
350
|
+
if (userScoped && !extra?.tenant_id) {
|
|
351
|
+
missing.push('tenant_id — this is a user-scoped connection: pass a "tenant_id" argument per call, or run list_my_tenants to see your options');
|
|
352
|
+
}
|
|
353
|
+
identity = {
|
|
354
|
+
transport: "http",
|
|
355
|
+
api_url: input.apiUrl,
|
|
356
|
+
key_type: keyType,
|
|
357
|
+
pat_present: keyType === "pat" || userScoped,
|
|
358
|
+
tenant_id: extra?.tenant_id ?? null,
|
|
359
|
+
organization_id: extra?.org_id ?? null,
|
|
360
|
+
user_id: extra?.user_id ?? null,
|
|
361
|
+
user_scoped: userScoped,
|
|
362
|
+
tenant_role: extra?.tenant_role ?? null,
|
|
363
|
+
org_role: extra?.org_role ?? null,
|
|
364
|
+
is_super_admin: extra?.is_super_admin === true,
|
|
365
|
+
scopes: input.authInfo?.scopes ?? [],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const summary = Object.entries(identity)
|
|
369
|
+
.map(([k, v]) => {
|
|
370
|
+
const rendered = v === null
|
|
371
|
+
? "—"
|
|
372
|
+
: Array.isArray(v)
|
|
373
|
+
? v.length
|
|
374
|
+
? v.join(", ")
|
|
375
|
+
: "—"
|
|
376
|
+
: String(v);
|
|
377
|
+
return `• ${k}: ${rendered}`;
|
|
378
|
+
})
|
|
379
|
+
.join("\n");
|
|
380
|
+
const missingBlock = missing.length
|
|
381
|
+
? `\n\nMissing / notes:\n${missing.map((m) => `⚠️ ${m}`).join("\n")}`
|
|
382
|
+
: "\n\n✅ Nothing missing for the current scope.";
|
|
383
|
+
return {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: `Dobby MCP — current identity\n\n${summary}${missingBlock}\n\n` +
|
|
388
|
+
"```json\n" +
|
|
389
|
+
JSON.stringify({ ...identity, missing }, null, 2) +
|
|
390
|
+
"\n```",
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
isError: false,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Synthetic `validate_credentials` tool. The sibling of `whoami`: where whoami
|
|
398
|
+
* ECHOES the session identity, this CONFIRMS the credentials actually work — it
|
|
399
|
+
* classifies the key, and in STDIO runs a live reachability probe so an
|
|
400
|
+
* onboarding agent can fail fast with one clear message instead of hunting
|
|
401
|
+
* through a scatter of downstream 401/404/500s. Like whoami it is a synthetic
|
|
402
|
+
* tool (registry stays 131), available in both transports, and NEVER throws —
|
|
403
|
+
* it reports what fails and the exact env var that supplies each credential.
|
|
404
|
+
* This is the MCP-callable counterpart of the `dobby-mcp --doctor` CLI (which an
|
|
405
|
+
* autonomous agent cannot run).
|
|
406
|
+
*/
|
|
407
|
+
const VALIDATE_CREDENTIALS_TOOL = "validate_credentials";
|
|
408
|
+
export const VALIDATE_CREDENTIALS_TOOL_DEF = {
|
|
409
|
+
name: VALIDATE_CREDENTIALS_TOOL,
|
|
410
|
+
description: "Check whether your configured credentials actually work. Reports, per " +
|
|
411
|
+
"credential, present / format_ok / authorized (a live reachability probe in " +
|
|
412
|
+
"STDIO) plus the exact env var that supplies it. Complements whoami (which " +
|
|
413
|
+
"only echoes identity). Never throws — it reports what fails. Call this if a " +
|
|
414
|
+
"tool returns an auth error and you are unsure which credential is the problem.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {},
|
|
418
|
+
additionalProperties: false,
|
|
419
|
+
},
|
|
420
|
+
annotations: {
|
|
421
|
+
title: "Validate credentials",
|
|
422
|
+
readOnlyHint: true,
|
|
423
|
+
destructiveHint: false,
|
|
424
|
+
openWorldHint: false,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* Run the credential validation. Never throws / never returns isError:true —
|
|
429
|
+
* failures are reported as `report.*.authorized=false` + `notes[]`.
|
|
430
|
+
*/
|
|
431
|
+
export async function handleValidateCredentials(input) {
|
|
432
|
+
const report = {
|
|
433
|
+
transport: input.mode,
|
|
434
|
+
api_url: input.apiUrl,
|
|
435
|
+
};
|
|
436
|
+
const notes = [];
|
|
437
|
+
if (input.mode === "stdio") {
|
|
438
|
+
const keyType = classifyKeyType(input.apiKey);
|
|
439
|
+
const present = keyType !== "none";
|
|
440
|
+
const formatOk = keyType === "gateway" || keyType === "tenant" || keyType === "pat";
|
|
441
|
+
// Live authorization probe — only when we have both a key and a tenant.
|
|
442
|
+
// checkHealth never throws (it catches internally), but guard anyway.
|
|
443
|
+
let authorized = null;
|
|
444
|
+
if (present && input.tenantId) {
|
|
445
|
+
try {
|
|
446
|
+
const probe = createDobbyClient({
|
|
447
|
+
apiUrl: input.apiUrl,
|
|
448
|
+
apiKey: input.apiKey,
|
|
449
|
+
tenantId: input.tenantId,
|
|
450
|
+
});
|
|
451
|
+
authorized = await probe.checkHealth();
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
authorized = false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (!present) {
|
|
458
|
+
notes.push("Set DOBBY_API_KEY — a gk_user_/gk_svc_ gateway key, an sk_live_ tenant key, or (remote only) a dobby_pat_.");
|
|
459
|
+
}
|
|
460
|
+
else if (!formatOk) {
|
|
461
|
+
notes.push("DOBBY_API_KEY has an unrecognized prefix — expected gk_user_/gk_svc_/gk_tmp_, sk_live_, or dobby_pat_.");
|
|
462
|
+
}
|
|
463
|
+
if (present && !input.tenantId) {
|
|
464
|
+
notes.push("Set DOBBY_TENANT_ID to run the live authorization probe (tenant-scoped tools also need it).");
|
|
465
|
+
}
|
|
466
|
+
if (authorized === false) {
|
|
467
|
+
notes.push(`Could not reach/authorize against ${input.apiUrl} — check DOBBY_API_URL, the key value, and the tenant.`);
|
|
468
|
+
}
|
|
469
|
+
if (keyType === "pat") {
|
|
470
|
+
notes.push("A dobby_pat_ does NOT enable gateway/execute tools in local STDIO — use a gk_ key, or the remote HTTP transport.");
|
|
471
|
+
}
|
|
472
|
+
report.api_key = {
|
|
473
|
+
present,
|
|
474
|
+
key_type: keyType,
|
|
475
|
+
format_ok: formatOk,
|
|
476
|
+
authorized,
|
|
477
|
+
env_var: "DOBBY_API_KEY",
|
|
478
|
+
};
|
|
479
|
+
report.gateway_tools = {
|
|
480
|
+
enabled: input.gatewayEnabled,
|
|
481
|
+
source: input.gatewaySource,
|
|
482
|
+
env_var: "DOBBY_GATEWAY_KEY",
|
|
483
|
+
};
|
|
484
|
+
report.tenant = {
|
|
485
|
+
present: Boolean(input.tenantId),
|
|
486
|
+
env_var: "DOBBY_TENANT_ID",
|
|
487
|
+
};
|
|
488
|
+
report.ok = present && formatOk && authorized !== false;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
const token = input.authInfo?.token;
|
|
492
|
+
const extra = input.authInfo?.extra;
|
|
493
|
+
const keyType = classifyKeyType(token);
|
|
494
|
+
const present = Boolean(token);
|
|
495
|
+
// We only reach here AFTER the token passed OAuth introspection, so an
|
|
496
|
+
// authenticated session is itself proof of authorization.
|
|
497
|
+
const authorized = present;
|
|
498
|
+
if (!present) {
|
|
499
|
+
notes.push("No bearer token — authenticate with OAuth or a dobby_pat_ against mcp.dobby-ai.com.");
|
|
500
|
+
}
|
|
501
|
+
if (extra?.user_scoped && !extra?.tenant_id) {
|
|
502
|
+
notes.push('User-scoped session: pass a "tenant_id" per call, or run list_my_tenants to see your options.');
|
|
503
|
+
}
|
|
504
|
+
report.api_key = {
|
|
505
|
+
present,
|
|
506
|
+
key_type: keyType,
|
|
507
|
+
format_ok: present,
|
|
508
|
+
authorized,
|
|
509
|
+
env_var: "Authorization: Bearer",
|
|
510
|
+
};
|
|
511
|
+
report.gateway_tools = {
|
|
512
|
+
enabled: true,
|
|
513
|
+
source: "gk_tmp_ minted per request from your session",
|
|
514
|
+
};
|
|
515
|
+
report.tenant = {
|
|
516
|
+
present: Boolean(extra?.tenant_id),
|
|
517
|
+
user_scoped: extra?.user_scoped === true,
|
|
518
|
+
};
|
|
519
|
+
report.scopes = input.authInfo?.scopes ?? [];
|
|
520
|
+
report.ok = present;
|
|
521
|
+
}
|
|
522
|
+
const notesBlock = notes.length
|
|
523
|
+
? `\n\nNotes:\n${notes.map((n) => `⚠️ ${n}`).join("\n")}`
|
|
524
|
+
: "\n\n✅ Credentials look good for the current scope.";
|
|
525
|
+
return {
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: "text",
|
|
529
|
+
text: "Dobby MCP — credential validation\n\n```json\n" +
|
|
530
|
+
JSON.stringify(report, null, 2) +
|
|
531
|
+
"\n```" +
|
|
532
|
+
notesBlock,
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
isError: false,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* ADR-020 user-scoped tool classification. A user-scoped PAT has no tenant/org
|
|
540
|
+
* pin, so an org-scoped tool must resolve its target ORG (not tenant) per call:
|
|
541
|
+
* - ORG_CLIENT_TOOLS read `client.organizationId` (memory stores / rubrics /
|
|
542
|
+
* orchestration configs). The caller passes an `org_id` argument, which the
|
|
543
|
+
* server sets on the per-request client; the org REST route then authorizes
|
|
544
|
+
* it live (withOrgAuth + tryPatOrgAuth → 403 for a non-member).
|
|
545
|
+
* - ORG_ARG_TOOLS already take an explicit `organization_id` in their own
|
|
546
|
+
* schema (the governance/policies tools), so they carry their own target —
|
|
547
|
+
* the user-scoped branch just must NOT demand a tenant_id for them.
|
|
548
|
+
* - PLATFORM_TOOLS are platform-scoped superadmin ops tools (health / status /
|
|
549
|
+
* issues / audit). They target NO tenant or org — they call the web app's
|
|
550
|
+
* /api/v1/internal/ops/* endpoints server-to-server (x-internal-secret), so
|
|
551
|
+
* the user-scoped branch must NOT demand a tenant_id for them either. Their
|
|
552
|
+
* visibility is still gated upstream by visibleTo({ isSuperAdmin }).
|
|
553
|
+
* Everything else is tenant-scoped and resolves from a per-call `tenant_id`.
|
|
554
|
+
*/
|
|
555
|
+
const ORG_CLIENT_TOOLS = new Set([
|
|
556
|
+
"memory_store_list",
|
|
557
|
+
"memory_store_read",
|
|
558
|
+
"memory_store_write",
|
|
559
|
+
"memory_store_search",
|
|
560
|
+
"memory_store_delete",
|
|
561
|
+
"rubric_list",
|
|
562
|
+
"rubric_create",
|
|
563
|
+
"orchestration_config_list",
|
|
564
|
+
]);
|
|
565
|
+
const ORG_ARG_TOOLS = new Set([
|
|
566
|
+
"export_policies",
|
|
567
|
+
"validate_policies",
|
|
568
|
+
"apply_guardrail",
|
|
569
|
+
"import_policies",
|
|
570
|
+
]);
|
|
571
|
+
// Platform-scoped superadmin ops tools (capability-core `superAdminOnly`). They
|
|
572
|
+
// act on platform-global ops data via /api/v1/internal/ops/* (server-to-server
|
|
573
|
+
// x-internal-secret), NOT on any tenant/org, so a user-scoped (PAT) connection
|
|
574
|
+
// must NOT be asked for a tenant_id. Visibility is still gated upstream by
|
|
575
|
+
// visibleTo({ isSuperAdmin }); this only stops the tenant-required default from
|
|
576
|
+
// rejecting them once visible (ADR-020).
|
|
577
|
+
const PLATFORM_TOOLS = new Set([
|
|
578
|
+
"get_ops_health_summary",
|
|
579
|
+
"get_service_status",
|
|
580
|
+
"get_active_issues",
|
|
581
|
+
"acknowledge_ops_issue",
|
|
582
|
+
"get_ops_audit_log",
|
|
583
|
+
]);
|
|
584
|
+
// ============================================
|
|
585
|
+
// Gateway Key Resolution
|
|
586
|
+
// ============================================
|
|
587
|
+
/**
|
|
588
|
+
* Resolve the gateway key to use for auth + audit logging.
|
|
589
|
+
* Priority: explicit DOBBY_GATEWAY_KEY > auto-detect gk_* in DOBBY_API_KEY > undefined (disabled)
|
|
590
|
+
*/
|
|
591
|
+
export function resolveGatewayKey(opts) {
|
|
592
|
+
if (opts.gatewayKey)
|
|
593
|
+
return opts.gatewayKey;
|
|
594
|
+
if (opts.apiKey && opts.apiKey.startsWith("gk_"))
|
|
595
|
+
return opts.apiKey;
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Create an MCP Server with all handlers registered.
|
|
600
|
+
*
|
|
601
|
+
* In STDIO mode: tenant context comes from config/env vars.
|
|
602
|
+
* In HTTP mode: tenant context comes from OAuth token (extra.authInfo.extra.tenant_id).
|
|
603
|
+
* The DobbyClient is created lazily per-request in HTTP mode.
|
|
604
|
+
*/
|
|
605
|
+
export async function createServer(opts = { mode: "stdio" }) {
|
|
606
|
+
const apiUrl = opts.apiUrl || process.env.DOBBY_API_URL || "https://dobby-ai.com";
|
|
607
|
+
// STDIO gateway auth context — resolved BEFORE the client so gateway-scoped
|
|
608
|
+
// fetch helpers send the gk_* downstream via client.gatewayBearer() (they
|
|
609
|
+
// previously always sent apiKey, so DOBBY_GATEWAY_KEY + sk_live_ setups
|
|
610
|
+
// never reached gateway-guarded routes).
|
|
611
|
+
const gatewayKey = resolveGatewayKey({
|
|
612
|
+
gatewayKey: opts.gatewayKey,
|
|
613
|
+
apiKey: opts.apiKey,
|
|
614
|
+
});
|
|
615
|
+
const gatewayUrl = opts.gatewayUrl || apiUrl;
|
|
616
|
+
// In STDIO mode, create a shared client from env vars
|
|
617
|
+
let stdioClient;
|
|
618
|
+
if (opts.mode === "stdio") {
|
|
619
|
+
stdioClient = createDobbyClient({
|
|
620
|
+
apiUrl: opts.apiUrl,
|
|
621
|
+
apiKey: opts.apiKey,
|
|
622
|
+
tenantId: opts.tenantId,
|
|
623
|
+
gatewayKey,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
// HTTP mode: per-SESSION memoized gateway-key resolution. createServer() is
|
|
627
|
+
// instantiated once per HTTP session (index.ts wires it as http-server.ts's
|
|
628
|
+
// createMcpServer factory, called on each new Mcp-Session-Id), so this
|
|
629
|
+
// closure caches ONE resolved gk_tmp_* key per session. Memoized on FIRST
|
|
630
|
+
// use: the OAuth token may rotate mid-session, but the resolved key stays
|
|
631
|
+
// valid — it is identity-bound (tenant+user), not token-bound.
|
|
632
|
+
//
|
|
633
|
+
// Tenant-pinned families (dobby_at_/sk_live_) use this single-key memo. A
|
|
634
|
+
// user-scoped PAT (ADR-020) has no pin — its gateway-scoped tools are
|
|
635
|
+
// tenant-scoped and carry a per-call tenant_id, so it memoizes a gk_tmp_ PER
|
|
636
|
+
// TARGET TENANT here (a session-wide key would be wrong for a cross-tenant
|
|
637
|
+
// caller).
|
|
638
|
+
let sessionGatewayKey;
|
|
639
|
+
const patGatewayKeyByTenant = new Map();
|
|
640
|
+
// Create MCP server
|
|
641
|
+
const server = new Server({
|
|
642
|
+
name: "dobby-mcp-server",
|
|
643
|
+
version: "1.1.0",
|
|
644
|
+
}, {
|
|
645
|
+
capabilities: {
|
|
646
|
+
tools: {},
|
|
647
|
+
resources: {},
|
|
648
|
+
prompts: {},
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
// STDIO user context
|
|
652
|
+
const userContext = {
|
|
653
|
+
userId: process.env.DOBBY_USER_ID || "mcp-user",
|
|
654
|
+
userName: process.env.DOBBY_USER_NAME || "MCP User",
|
|
655
|
+
};
|
|
656
|
+
// ============================================
|
|
657
|
+
// Tools Handler
|
|
658
|
+
// ============================================
|
|
659
|
+
server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
|
|
660
|
+
// HTTP mode: the introspection endpoint put the EFFECTIVE mcp scope on
|
|
661
|
+
// the token (plan-derived tier + read/execute) — filter the list by it.
|
|
662
|
+
// STDIO mode stays full access (local setup, key already proves tenant).
|
|
663
|
+
if (opts.mode === "http") {
|
|
664
|
+
const scopes = extra.authInfo?.scopes ?? [];
|
|
665
|
+
const rbac = extra.authInfo?.extra;
|
|
666
|
+
const tools = getToolDefinitions(resolveToolTier({ scopes }), {
|
|
667
|
+
readOnly: isReadOnlyScopes(scopes),
|
|
668
|
+
role: rbac?.tenant_role,
|
|
669
|
+
isSuperAdmin: rbac?.is_super_admin,
|
|
670
|
+
});
|
|
671
|
+
// whoami + validate_credentials are always available (identity echo +
|
|
672
|
+
// credential check) — appended after the tier-filtered list so they show
|
|
673
|
+
// for every token family.
|
|
674
|
+
tools.push(WHOAMI_TOOL_DEF);
|
|
675
|
+
tools.push(VALIDATE_CREDENTIALS_TOOL_DEF);
|
|
676
|
+
// ADR-020: a user-scoped session also gets the synthetic discovery tools
|
|
677
|
+
// so a client can enumerate the tenants + organizations it may act in.
|
|
678
|
+
// Append-only — a tenant-pinned session (and the byte-identical golden
|
|
679
|
+
// tests) is unchanged.
|
|
680
|
+
if (rbac?.user_scoped) {
|
|
681
|
+
tools.push(USER_SCOPED_TENANTS_TOOL_DEF);
|
|
682
|
+
tools.push(USER_SCOPED_ORGS_TOOL_DEF);
|
|
683
|
+
}
|
|
684
|
+
return { tools };
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
tools: [
|
|
688
|
+
...getToolDefinitions("enterprise"),
|
|
689
|
+
WHOAMI_TOOL_DEF,
|
|
690
|
+
VALIDATE_CREDENTIALS_TOOL_DEF,
|
|
691
|
+
],
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
695
|
+
const { name, arguments: args } = request.params;
|
|
696
|
+
try {
|
|
697
|
+
// whoami — always available, never gated, never errors. Answer identity
|
|
698
|
+
// BEFORE any tier/tenant resolution so a caller can orient even when its
|
|
699
|
+
// credentials are missing or ambiguous.
|
|
700
|
+
if (name === WHOAMI_TOOL) {
|
|
701
|
+
return opts.mode === "http"
|
|
702
|
+
? handleWhoami({
|
|
703
|
+
mode: "http",
|
|
704
|
+
apiUrl,
|
|
705
|
+
authInfo: extra.authInfo,
|
|
706
|
+
})
|
|
707
|
+
: handleWhoami({
|
|
708
|
+
mode: "stdio",
|
|
709
|
+
apiUrl,
|
|
710
|
+
apiKey: opts.apiKey,
|
|
711
|
+
tenantId: opts.tenantId ?? process.env.DOBBY_TENANT_ID,
|
|
712
|
+
organizationId: process.env.DOBBY_ORGANIZATION_ID,
|
|
713
|
+
gatewayEnabled: Boolean(gatewayKey),
|
|
714
|
+
userId: userContext.userId,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
// validate_credentials — like whoami, always available and never gated.
|
|
718
|
+
// In STDIO it runs a live reachability probe, so it is async.
|
|
719
|
+
if (name === VALIDATE_CREDENTIALS_TOOL) {
|
|
720
|
+
return opts.mode === "http"
|
|
721
|
+
? await handleValidateCredentials({
|
|
722
|
+
mode: "http",
|
|
723
|
+
apiUrl,
|
|
724
|
+
authInfo: extra.authInfo,
|
|
725
|
+
})
|
|
726
|
+
: await handleValidateCredentials({
|
|
727
|
+
mode: "stdio",
|
|
728
|
+
apiUrl,
|
|
729
|
+
apiKey: opts.apiKey,
|
|
730
|
+
tenantId: opts.tenantId ?? process.env.DOBBY_TENANT_ID,
|
|
731
|
+
gatewayEnabled: Boolean(gatewayKey),
|
|
732
|
+
gatewaySource: opts.gatewayKey
|
|
733
|
+
? "explicit"
|
|
734
|
+
: opts.apiKey?.startsWith("gk_")
|
|
735
|
+
? "api_key"
|
|
736
|
+
: "none",
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
// HTTP mode: enforce tier + read-only on the CALL, not just the list —
|
|
740
|
+
// list filtering alone is cosmetic (clients can call any tool by name).
|
|
741
|
+
if (opts.mode === "http") {
|
|
742
|
+
const rbac = extra.authInfo?.extra;
|
|
743
|
+
const denial = getToolAccessDenial(name, {
|
|
744
|
+
scopes: extra.authInfo?.scopes ?? [],
|
|
745
|
+
role: rbac?.tenant_role,
|
|
746
|
+
isSuperAdmin: rbac?.is_super_admin,
|
|
747
|
+
});
|
|
748
|
+
if (denial) {
|
|
749
|
+
return {
|
|
750
|
+
content: [{ type: "text", text: denial }],
|
|
751
|
+
isError: true,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
let client;
|
|
756
|
+
let userId;
|
|
757
|
+
let userName;
|
|
758
|
+
let tenantId;
|
|
759
|
+
let region;
|
|
760
|
+
let gwKey;
|
|
761
|
+
let gwUrl;
|
|
762
|
+
// ADR-019/020 identity, threaded into the CapabilityContext so handlers
|
|
763
|
+
// can authorize per call (Wave 2/3). Empty in STDIO mode (local, full
|
|
764
|
+
// access). For a user-scoped PAT tenant_role/org_role are the AGGREGATE role.
|
|
765
|
+
let ctxOrgId;
|
|
766
|
+
let ctxScopes;
|
|
767
|
+
let ctxTenantRole;
|
|
768
|
+
let ctxOrgRole;
|
|
769
|
+
let ctxIsSuperAdmin = false;
|
|
770
|
+
if (opts.mode === "http" && extra.authInfo) {
|
|
771
|
+
// HTTP mode — resolve context from the introspected token.
|
|
772
|
+
const tokenExtra = extra.authInfo.extra;
|
|
773
|
+
const token = extra.authInfo.token;
|
|
774
|
+
const isGatewayKeyToken = token.startsWith("gk_");
|
|
775
|
+
const userScoped = tokenExtra?.user_scoped === true;
|
|
776
|
+
const tokenUserId = tokenExtra?.user_id || "oauth-user";
|
|
777
|
+
// Identity carried from introspection (ADR-019 P2 / ADR-020). For a
|
|
778
|
+
// user-scoped PAT, tenant_role/org_role are the AGGREGATE (max) role.
|
|
779
|
+
ctxOrgId = tokenExtra?.org_id ?? undefined;
|
|
780
|
+
ctxScopes = extra.authInfo.scopes ?? [];
|
|
781
|
+
ctxTenantRole = tokenExtra?.tenant_role;
|
|
782
|
+
ctxOrgRole = tokenExtra?.org_role;
|
|
783
|
+
ctxIsSuperAdmin = tokenExtra?.is_super_admin === true;
|
|
784
|
+
// User-scoped discovery tools (ADR-020): not registered capabilities,
|
|
785
|
+
// so intercept them before any tenant/org resolution or client build.
|
|
786
|
+
// They fetch directly (apiUrl + token) and need no per-request client.
|
|
787
|
+
if (userScoped && name === USER_SCOPED_TENANTS_TOOL) {
|
|
788
|
+
return await handleListMyTenants(apiUrl, token);
|
|
789
|
+
}
|
|
790
|
+
if (userScoped && name === USER_SCOPED_ORGS_TOOL) {
|
|
791
|
+
return await handleListMyOrgs(apiUrl, token);
|
|
792
|
+
}
|
|
793
|
+
// Resolve the TARGET tenant/org.
|
|
794
|
+
// - Pinned families (dobby_at_/sk_live_/gk_): the token's single tenant
|
|
795
|
+
// pin (org-scoped tools resolve their org downstream from the pin).
|
|
796
|
+
// - User-scoped PAT (ADR-020): the target comes from a per-call
|
|
797
|
+
// argument, authorized LIVE by the web REST layer (never a frozen
|
|
798
|
+
// cross-tenant/cross-org grant):
|
|
799
|
+
// · ORG_CLIENT_TOOLS → an `org_id` arg, set on the per-request
|
|
800
|
+
// client so the handler's /organizations/{org}/… call is
|
|
801
|
+
// authorized by withOrgAuth + tryPatOrgAuth (403 for a non-member).
|
|
802
|
+
// · ORG_ARG_TOOLS → the tool's own `organization_id` arg carries
|
|
803
|
+
// the target; nothing to require here.
|
|
804
|
+
// · everything else is tenant-scoped → a `tenant_id` arg
|
|
805
|
+
// (withTenantAuth + tryPatAuth → 403 for a non-member).
|
|
806
|
+
let targetTenantId;
|
|
807
|
+
let targetOrgId;
|
|
808
|
+
if (userScoped) {
|
|
809
|
+
if (ORG_CLIENT_TOOLS.has(name)) {
|
|
810
|
+
const rawOrgArg = args
|
|
811
|
+
?.org_id;
|
|
812
|
+
const argOrg = typeof rawOrgArg === "string" ? rawOrgArg.trim() : "";
|
|
813
|
+
if (!argOrg) {
|
|
814
|
+
return needsSelectionResult("organization", "org_id", USER_SCOPED_ORGS_TOOL);
|
|
815
|
+
}
|
|
816
|
+
targetOrgId = argOrg;
|
|
817
|
+
}
|
|
818
|
+
else if (ORG_ARG_TOOLS.has(name)) {
|
|
819
|
+
// The tool carries its own `organization_id`; its org REST route
|
|
820
|
+
// authorizes the PAT live against it. No tenant/org to resolve here.
|
|
821
|
+
}
|
|
822
|
+
else if (PLATFORM_TOOLS.has(name)) {
|
|
823
|
+
// Platform-scoped ops tool: targets no tenant/org (it hits
|
|
824
|
+
// /api/v1/internal/ops/* server-to-server). Nothing to resolve here;
|
|
825
|
+
// visibility is already gated by visibleTo({ isSuperAdmin }).
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
const rawTenantArg = args
|
|
829
|
+
?.tenant_id;
|
|
830
|
+
const argTenant = typeof rawTenantArg === "string" ? rawTenantArg.trim() : "";
|
|
831
|
+
if (!argTenant) {
|
|
832
|
+
// Short-prompt UX: if the caller belongs to exactly ONE tenant,
|
|
833
|
+
// auto-select it silently — no needs_selection round-trip, so
|
|
834
|
+
// "create an agent named X" works from a single prompt. Ambiguous
|
|
835
|
+
// cases (zero or many tenants) still return the structured
|
|
836
|
+
// needs_selection envelope so the client asks which workspace.
|
|
837
|
+
const sole = await resolveSoleTenant(apiUrl, token);
|
|
838
|
+
if (!sole) {
|
|
839
|
+
return needsSelectionResult("tenant", "tenant_id", USER_SCOPED_TENANTS_TOOL);
|
|
840
|
+
}
|
|
841
|
+
targetTenantId = sole;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
targetTenantId = argTenant || undefined;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
targetTenantId = tokenExtra?.tenant_id;
|
|
850
|
+
if (!targetTenantId) {
|
|
851
|
+
return {
|
|
852
|
+
content: [
|
|
853
|
+
{
|
|
854
|
+
type: "text",
|
|
855
|
+
text: "Error: OAuth token missing tenant_id",
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
isError: true,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
// Tenant-pinned families (dobby_at_ / sk_live_ / gk_) carry their org
|
|
862
|
+
// in the token — wire it onto the per-request client so ORG-scoped
|
|
863
|
+
// tools (memory_store_*, rubric_*, orchestration_config_list) work.
|
|
864
|
+
// Those handlers read client.organizationId; without this a
|
|
865
|
+
// tenant-pinned OAuth/sk_live_ session got "No organization context"
|
|
866
|
+
// for every org-scoped tool even though whoami showed the org
|
|
867
|
+
// (customer hit 2026-07-20). user-scoped PATs resolve org per-call
|
|
868
|
+
// above, so this only fills the pinned case.
|
|
869
|
+
targetOrgId = tokenExtra?.org_id ?? undefined;
|
|
870
|
+
}
|
|
871
|
+
// Reflect the per-call org target in the identity context too, so the
|
|
872
|
+
// CapabilityContext.orgId a handler sees matches the org it acts on.
|
|
873
|
+
if (targetOrgId)
|
|
874
|
+
ctxOrgId = targetOrgId;
|
|
875
|
+
// Create per-request client using the session bearer as the API key
|
|
876
|
+
// (tenant-scoped tools send it as X-API-Key, which the web app accepts
|
|
877
|
+
// for all families: dobby_at_ / sk_live_ / gk_ / dobby_pat_).
|
|
878
|
+
//
|
|
879
|
+
// Gateway-scoped tools authenticate downstream with a gk_* key
|
|
880
|
+
// (authenticateGatewayRequest hard-requires one). The provider that
|
|
881
|
+
// supplies it differs by session family:
|
|
882
|
+
// - gk_* Bearer session: the token IS a gateway key — no provider;
|
|
883
|
+
// gatewayBearer() falls back to apiKey and sends it downstream.
|
|
884
|
+
// - dobby_at_ / sk_live_ session: a LAZY provider exchanges the token
|
|
885
|
+
// for a tenant+user-bound gk_tmp_* via the web app's internal
|
|
886
|
+
// endpoint (tenant derived from the token), memoized once per session.
|
|
887
|
+
// - user-scoped PAT (ADR-020): gateway-scoped tools are tenant-scoped
|
|
888
|
+
// and carry a per-call tenant_id, so the provider resolves a gk_tmp_*
|
|
889
|
+
// for THAT tenant (memoized per tenant). The internal endpoint
|
|
890
|
+
// authorizes the PAT's LIVE membership in the tenant before minting,
|
|
891
|
+
// so no frozen cross-tenant grant is possible. Org-scoped tools have
|
|
892
|
+
// no tenant → no gateway provider (they are not gateway-scoped).
|
|
893
|
+
let getGatewayKey;
|
|
894
|
+
if (isGatewayKeyToken) {
|
|
895
|
+
getGatewayKey = undefined;
|
|
896
|
+
}
|
|
897
|
+
else if (userScoped) {
|
|
898
|
+
if (targetTenantId) {
|
|
899
|
+
const t = targetTenantId;
|
|
900
|
+
getGatewayKey = () => {
|
|
901
|
+
let p = patGatewayKeyByTenant.get(t);
|
|
902
|
+
if (!p) {
|
|
903
|
+
p = resolveSessionGatewayKey({ apiUrl, token, tenantId: t });
|
|
904
|
+
patGatewayKeyByTenant.set(t, p);
|
|
905
|
+
}
|
|
906
|
+
return p;
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
getGatewayKey = () => (sessionGatewayKey ??= resolveSessionGatewayKey({ apiUrl, token }));
|
|
912
|
+
}
|
|
913
|
+
client = new DobbyClient({
|
|
914
|
+
apiUrl,
|
|
915
|
+
apiKey: token,
|
|
916
|
+
tenantId: targetTenantId ?? "",
|
|
917
|
+
// ADR-020 org-scoped tools (ORG_CLIENT_TOOLS): the per-call `org_id`
|
|
918
|
+
// becomes the client's org context, which their handlers read via
|
|
919
|
+
// client.organizationId to build /organizations/{org}/… requests.
|
|
920
|
+
organizationId: targetOrgId,
|
|
921
|
+
...(getGatewayKey ? { getGatewayKey } : {}),
|
|
922
|
+
});
|
|
923
|
+
userId = tokenUserId;
|
|
924
|
+
userName = "OAuth User";
|
|
925
|
+
tenantId = targetTenantId;
|
|
926
|
+
region = undefined; // resolved by web app from tenant
|
|
927
|
+
// Gateway pre-check key (checkGatewayAuth → POST /api/v1/gateway/mcp/tools):
|
|
928
|
+
// - gk_* Bearer session: forward the token — restores the pre-check +
|
|
929
|
+
// gateway metering for genuine gateway-key sessions (commit 157db598b
|
|
930
|
+
// disabled it for ALL families; only OAuth/sk_live_ needed the skip).
|
|
931
|
+
// - dobby_at_ / sk_live_ / user-scoped session: leave it undefined —
|
|
932
|
+
// the pre-check authenticates a gk_* GATEWAY key, and forwarding a
|
|
933
|
+
// non-gk_ token made the gateway 404 → every tool denied "Not Found"
|
|
934
|
+
// (found 2026-07-12 on the live claude.ai connector). Unset correctly
|
|
935
|
+
// short-circuits the pre-check to allowed; the REAL gate for
|
|
936
|
+
// gateway-scoped endpoints is downstream authenticateGatewayRequest,
|
|
937
|
+
// reached with the resolved gk_tmp_* (client.gatewayBearer()).
|
|
938
|
+
gwKey = isGatewayKeyToken ? token : undefined;
|
|
939
|
+
gwUrl = apiUrl;
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
// STDIO mode — use env vars
|
|
943
|
+
client = stdioClient;
|
|
944
|
+
userId = userContext.userId;
|
|
945
|
+
userName = userContext.userName;
|
|
946
|
+
tenantId = client.tenantId;
|
|
947
|
+
region = process.env.DOBBY_REGION;
|
|
948
|
+
gwKey = gatewayKey;
|
|
949
|
+
gwUrl = gatewayUrl;
|
|
950
|
+
}
|
|
951
|
+
const result = await handleToolCall(name, args || {}, {
|
|
952
|
+
client,
|
|
953
|
+
userId,
|
|
954
|
+
userName,
|
|
955
|
+
gatewayKey: gwKey,
|
|
956
|
+
gatewayUrl: gwUrl,
|
|
957
|
+
tenantId,
|
|
958
|
+
region,
|
|
959
|
+
orgId: ctxOrgId,
|
|
960
|
+
scopes: ctxScopes,
|
|
961
|
+
tenantRole: ctxTenantRole,
|
|
962
|
+
orgRole: ctxOrgRole,
|
|
963
|
+
isSuperAdmin: ctxIsSuperAdmin,
|
|
964
|
+
});
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
return {
|
|
969
|
+
content: [
|
|
970
|
+
{
|
|
971
|
+
type: "text",
|
|
972
|
+
text: `Error: ${error.message}`,
|
|
973
|
+
},
|
|
974
|
+
],
|
|
975
|
+
isError: true,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
// ============================================
|
|
980
|
+
// Resources Handler
|
|
981
|
+
// ============================================
|
|
982
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
983
|
+
return {
|
|
984
|
+
resources: resourceDefinitions,
|
|
985
|
+
resourceTemplates: resourceTemplates,
|
|
986
|
+
};
|
|
987
|
+
});
|
|
988
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
989
|
+
const { uri } = request.params;
|
|
990
|
+
const client = stdioClient || new DobbyClient({ apiUrl, apiKey: "", tenantId: "" });
|
|
991
|
+
try {
|
|
992
|
+
return await handleResourceRead(uri, client);
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
throw new Error(`Failed to read resource ${uri}: ${error.message}`);
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
// ============================================
|
|
999
|
+
// Prompts Handler
|
|
1000
|
+
// ============================================
|
|
1001
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1002
|
+
return {
|
|
1003
|
+
prompts: promptDefinitions,
|
|
1004
|
+
};
|
|
1005
|
+
});
|
|
1006
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1007
|
+
const { name, arguments: args } = request.params;
|
|
1008
|
+
try {
|
|
1009
|
+
return handlePromptGet(name, args || {});
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
throw new Error(`Failed to get prompt ${name}: ${error.message}`);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
return server;
|
|
1016
|
+
}
|
|
1017
|
+
//# sourceMappingURL=server.js.map
|