@databasin/mcp-client 0.1.0 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +128 -61
  2. package/dist/abort.d.ts +5 -0
  3. package/dist/abort.d.ts.map +1 -0
  4. package/dist/abort.js +25 -0
  5. package/dist/abort.js.map +1 -0
  6. package/dist/api-client.d.ts +49 -34
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +142 -145
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/api-contract.d.ts +59 -0
  11. package/dist/api-contract.d.ts.map +1 -0
  12. package/dist/api-contract.js +47 -0
  13. package/dist/api-contract.js.map +1 -0
  14. package/dist/api-profile.d.ts +5 -0
  15. package/dist/api-profile.d.ts.map +1 -0
  16. package/dist/api-profile.js +38 -0
  17. package/dist/api-profile.js.map +1 -0
  18. package/dist/api-request.d.ts +27 -0
  19. package/dist/api-request.d.ts.map +1 -0
  20. package/dist/api-request.js +147 -0
  21. package/dist/api-request.js.map +1 -0
  22. package/dist/api-response.d.ts +9 -0
  23. package/dist/api-response.d.ts.map +1 -0
  24. package/dist/api-response.js +206 -0
  25. package/dist/api-response.js.map +1 -0
  26. package/dist/auth.d.ts +22 -11
  27. package/dist/auth.d.ts.map +1 -1
  28. package/dist/auth.js +71 -71
  29. package/dist/auth.js.map +1 -1
  30. package/dist/browser.d.ts +20 -1
  31. package/dist/browser.d.ts.map +1 -1
  32. package/dist/browser.js +19 -17
  33. package/dist/browser.js.map +1 -1
  34. package/dist/cli.d.ts +31 -2
  35. package/dist/cli.d.ts.map +1 -1
  36. package/dist/cli.js +147 -67
  37. package/dist/cli.js.map +1 -1
  38. package/dist/config.js +2 -2
  39. package/dist/config.js.map +1 -1
  40. package/dist/correlation.d.ts +6 -0
  41. package/dist/correlation.d.ts.map +1 -0
  42. package/dist/correlation.js +15 -0
  43. package/dist/correlation.js.map +1 -0
  44. package/dist/credential-store.d.ts +21 -0
  45. package/dist/credential-store.d.ts.map +1 -0
  46. package/dist/credential-store.js +46 -0
  47. package/dist/credential-store.js.map +1 -0
  48. package/dist/errors.d.ts +17 -3
  49. package/dist/errors.d.ts.map +1 -1
  50. package/dist/errors.js +24 -6
  51. package/dist/errors.js.map +1 -1
  52. package/dist/login-url.d.ts +18 -0
  53. package/dist/login-url.d.ts.map +1 -0
  54. package/dist/login-url.js +82 -0
  55. package/dist/login-url.js.map +1 -0
  56. package/dist/safety.d.ts +4 -0
  57. package/dist/safety.d.ts.map +1 -1
  58. package/dist/safety.js +94 -10
  59. package/dist/safety.js.map +1 -1
  60. package/dist/schemas.d.ts +326 -49
  61. package/dist/schemas.d.ts.map +1 -1
  62. package/dist/schemas.js +242 -55
  63. package/dist/schemas.js.map +1 -1
  64. package/dist/server.d.ts +2 -4
  65. package/dist/server.d.ts.map +1 -1
  66. package/dist/server.js +17 -10
  67. package/dist/server.js.map +1 -1
  68. package/dist/token-validation.d.ts +5 -0
  69. package/dist/token-validation.d.ts.map +1 -0
  70. package/dist/token-validation.js +183 -0
  71. package/dist/token-validation.js.map +1 -0
  72. package/dist/tools.d.ts +12 -1
  73. package/dist/tools.d.ts.map +1 -1
  74. package/dist/tools.js +286 -75
  75. package/dist/tools.js.map +1 -1
  76. package/dist/version.d.ts +1 -1
  77. package/dist/version.d.ts.map +1 -1
  78. package/dist/version.js +1 -1
  79. package/dist/version.js.map +1 -1
  80. package/package.json +10 -6
  81. package/dist/mappers.d.ts +0 -56
  82. package/dist/mappers.d.ts.map +0 -1
  83. package/dist/mappers.js +0 -171
  84. package/dist/mappers.js.map +0 -1
@@ -0,0 +1,183 @@
1
+ import { DataBasinError } from './errors.js';
2
+ const MAX_ACCESS_TOKEN_BYTES = 64 * 1024;
3
+ const MAX_JWT_PAYLOAD_BYTES = 32 * 1024;
4
+ const TOKEN_REJECTED_MESSAGE = 'Microsoft Entra returned an access token that did not match the configured DataBasin API.';
5
+ /**
6
+ * MSAL records the account's environment as whichever canonical alias Entra's
7
+ * instance-discovery metadata prefers — observed as login.windows.net in
8
+ * practice (2026-08-26), which the previous exact pin of
9
+ * login.microsoftonline.com rejected, invalidating every successful sign-in.
10
+ * These are the four first-party aliases from Entra's published instance
11
+ * metadata for the worldwide cloud; anything else is still rejected, and the
12
+ * token's issuer/audience/scope/tenant checks are unchanged.
13
+ */
14
+ const ENTRA_ACCOUNT_ENVIRONMENTS = new Set([
15
+ 'login.microsoftonline.com',
16
+ 'login.windows.net',
17
+ 'login.microsoft.com',
18
+ 'sts.windows.net'
19
+ ]);
20
+ export function accountMatchesProfile(account, profile) {
21
+ return sameTenant(account.tenantId, profile.tenantId)
22
+ && ENTRA_ACCOUNT_ENVIRONMENTS.has(account.environment.toLowerCase());
23
+ }
24
+ export function validateAccessTokenResult(result, profile, nowMs) {
25
+ assertAuthenticationResult(result);
26
+ assertMsalMetadata(result, profile, nowMs);
27
+ const requirements = tokenRequirements(profile);
28
+ if (!hasRequiredMsalScope(result.scopes, requirements))
29
+ rejectToken();
30
+ const claims = decodeJwtPayload(result.accessToken);
31
+ assertTokenClaims(claims, profile, requirements, nowMs);
32
+ // This local payload check is defense-in-depth only. The DataBasin API remains
33
+ // responsible for cryptographic signature and authorization validation.
34
+ return result.accessToken;
35
+ }
36
+ function assertAuthenticationResult(result) {
37
+ if (!result)
38
+ rejectToken();
39
+ if (!result.accessToken)
40
+ rejectToken();
41
+ if (!result.account)
42
+ rejectToken();
43
+ }
44
+ function assertMsalMetadata(result, profile, nowMs) {
45
+ if (!Number.isFinite(nowMs))
46
+ rejectToken();
47
+ if (!accountMatchesProfile(result.account, profile))
48
+ rejectToken();
49
+ if (!sameTenant(result.tenantId, profile.tenantId))
50
+ rejectToken();
51
+ if (!matchesConfiguredAuthority(result.authority, profile.tenantId))
52
+ rejectToken();
53
+ if (result.tokenType.toLowerCase() !== 'bearer')
54
+ rejectToken();
55
+ if (!(result.expiresOn instanceof Date))
56
+ rejectToken();
57
+ const expiresOn = result.expiresOn.getTime();
58
+ if (!Number.isFinite(expiresOn) || expiresOn <= nowMs)
59
+ rejectToken();
60
+ }
61
+ function assertTokenClaims(claims, profile, requirements, nowMs) {
62
+ if (!sameTenant(stringClaim(claims, 'tid'), profile.tenantId))
63
+ rejectToken();
64
+ const issuer = stringClaim(claims, 'iss').toLowerCase();
65
+ if (!allowedIssuers(profile.tenantId).has(issuer))
66
+ rejectToken();
67
+ if (!matchesAudience(stringClaim(claims, 'aud'), requirements))
68
+ rejectToken();
69
+ if (!hasDelegatedScope(stringClaim(claims, 'scp'), requirements.delegatedScope))
70
+ rejectToken();
71
+ const nowSeconds = Math.floor(nowMs / 1_000);
72
+ if (numericDateClaim(claims, 'exp') <= nowSeconds)
73
+ rejectToken();
74
+ if (numericDateClaim(claims, 'nbf') > nowSeconds)
75
+ rejectToken();
76
+ }
77
+ function matchesAudience(audience, requirements) {
78
+ return audience === requirements.resourceUri || audience === requirements.resourceClientId;
79
+ }
80
+ function hasDelegatedScope(rawScopes, requiredScope) {
81
+ const expected = requiredScope.toLowerCase();
82
+ return rawScopes.split(/\s+/u).filter(Boolean).some((scope) => scope.toLowerCase() === expected);
83
+ }
84
+ function tokenRequirements(profile) {
85
+ if (profile.scopes.length !== 1)
86
+ rejectToken();
87
+ const configuredScope = profile.scopes[0];
88
+ const match = /^api:\/\/([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\/([A-Za-z0-9._-]{1,128})$/iu.exec(configuredScope);
89
+ if (!match)
90
+ rejectToken();
91
+ return {
92
+ resourceUri: `api://${match[1]}`,
93
+ resourceClientId: match[1],
94
+ configuredScope,
95
+ delegatedScope: match[2]
96
+ };
97
+ }
98
+ function hasRequiredMsalScope(scopes, requirements) {
99
+ const accepted = new Set([
100
+ requirements.configuredScope.toLowerCase(),
101
+ requirements.delegatedScope.toLowerCase()
102
+ ]);
103
+ return scopes.some((scope) => accepted.has(scope.trim().toLowerCase()));
104
+ }
105
+ function matchesConfiguredAuthority(raw, tenantId) {
106
+ try {
107
+ const authority = new URL(raw);
108
+ const expectedPath = `/${tenantId.toLowerCase()}`;
109
+ const path = authority.pathname.toLowerCase();
110
+ const safeOrigin = [
111
+ authority.protocol === 'https:',
112
+ ENTRA_ACCOUNT_ENVIRONMENTS.has(authority.hostname.toLowerCase()),
113
+ authority.port === '',
114
+ authority.username === '',
115
+ authority.password === '',
116
+ authority.search === '',
117
+ authority.hash === ''
118
+ ].every(Boolean);
119
+ return safeOrigin && (path === expectedPath || path === `${expectedPath}/`);
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ function allowedIssuers(tenantId) {
126
+ const tenant = tenantId.toLowerCase();
127
+ return new Set([
128
+ `https://sts.windows.net/${tenant}/`,
129
+ `https://login.microsoftonline.com/${tenant}/v2.0`
130
+ ]);
131
+ }
132
+ function decodeJwtPayload(accessToken) {
133
+ if (Buffer.byteLength(accessToken, 'utf8') > MAX_ACCESS_TOKEN_BYTES)
134
+ rejectToken();
135
+ const segments = accessToken.split('.');
136
+ if (segments.length !== 3)
137
+ rejectToken();
138
+ if (segments.some((segment) => !/^[A-Za-z0-9_-]+$/u.test(segment)))
139
+ rejectToken();
140
+ const payloadSegment = segments[1];
141
+ if (payloadSegment.length % 4 === 1)
142
+ rejectToken();
143
+ return parseJwtPayload(payloadSegment);
144
+ }
145
+ function parseJwtPayload(payloadSegment) {
146
+ try {
147
+ const bytes = Buffer.from(payloadSegment, 'base64url');
148
+ if (bytes.length > MAX_JWT_PAYLOAD_BYTES)
149
+ rejectToken();
150
+ if (bytes.toString('base64url') !== payloadSegment)
151
+ rejectToken();
152
+ const parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
153
+ if (!parsed || typeof parsed !== 'object')
154
+ rejectToken();
155
+ if (Array.isArray(parsed))
156
+ rejectToken();
157
+ return parsed;
158
+ }
159
+ catch (error) {
160
+ if (error instanceof DataBasinError)
161
+ throw error;
162
+ rejectToken();
163
+ }
164
+ }
165
+ function stringClaim(claims, name) {
166
+ const value = claims[name];
167
+ if (typeof value !== 'string' || value.length === 0 || value.length > 2_048)
168
+ rejectToken();
169
+ return value;
170
+ }
171
+ function numericDateClaim(claims, name) {
172
+ const value = claims[name];
173
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
174
+ rejectToken();
175
+ return value;
176
+ }
177
+ function sameTenant(actual, expected) {
178
+ return actual.toLowerCase() === expected.toLowerCase();
179
+ }
180
+ function rejectToken() {
181
+ throw new DataBasinError('UNAUTHENTICATED', TOKEN_REJECTED_MESSAGE);
182
+ }
183
+ //# sourceMappingURL=token-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-validation.js","sourceRoot":"","sources":["../src/token-validation.ts"],"names":[],"mappings":"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"}
package/dist/tools.d.ts CHANGED
@@ -2,7 +2,16 @@ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import type { DataBasinApi } from './api-client.js';
3
3
  import type { AssistantAuthResult } from './auth.js';
4
4
  import type { DataBasinProfile } from './config.js';
5
- export declare const TOOL_NAMES: readonly ["databasin_auth_status", "databasin_login", "databasin_get_context", "databasin_search", "databasin_browse_schema", "databasin_get_operation", "databasin_cancel_operation"];
5
+ export declare const LOCAL_AUTH_TOOL_NAMES: readonly ["databasin_auth_status", "databasin_login"];
6
+ export declare const REMOTE_PRODUCT_TOOL_NAMES: readonly ["databasin_get_capabilities", "databasin_get_context", "databasin_search", "databasin_get_schema", "databasin_run_agent", "databasin_get_agent_run", "databasin_cancel_agent_run", "databasin_run_query", "databasin_get_query_status", "databasin_cancel_query", "databasin_run_assistant"];
7
+ export declare const TOOL_NAMES: readonly ["databasin_auth_status", "databasin_login", "databasin_get_capabilities", "databasin_get_context", "databasin_search", "databasin_get_schema", "databasin_run_agent", "databasin_get_agent_run", "databasin_cancel_agent_run", "databasin_run_query", "databasin_get_query_status", "databasin_cancel_query", "databasin_run_assistant"];
8
+ type PollingOptions = {
9
+ maxWaitMs: number;
10
+ initialDelayMs: number;
11
+ maxDelayMs: number;
12
+ sleep: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
13
+ now: () => number;
14
+ };
6
15
  interface Dependencies {
7
16
  api: DataBasinApi;
8
17
  auth: {
@@ -11,6 +20,8 @@ interface Dependencies {
11
20
  };
12
21
  profile: DataBasinProfile;
13
22
  maxOutputBytes?: number;
23
+ polling?: Partial<PollingOptions>;
24
+ queryPolling?: Partial<PollingOptions>;
14
25
  }
15
26
  export declare function registerDataBasinTools(server: McpServer, dependencies: Dependencies): void;
16
27
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,8BAA8B,CAAC;AAG3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAiBpD,eAAO,MAAM,UAAU,wLAQZ,CAAC;AAEZ,UAAU,YAAY;IACpB,GAAG,EAAE,YAAY,CAAC;IAClB,IAAI,EAAE;QACJ,UAAU,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC3C,cAAc,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;KAChD,CAAC;IACF,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,CAkG1F"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAsB,MAAM,8BAA8B,CAAC;AAI7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA0BpD,eAAO,MAAM,qBAAqB,uDAGvB,CAAC;AAEZ,eAAO,MAAM,yBAAyB,wSAY3B,CAAC;AAEZ,eAAO,MAAM,UAAU,oVAGZ,CAAC;AAEZ,KAAK,cAAc,GAAG;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB,CAAC;AAEF,UAAU,YAAY;IACpB,GAAG,EAAE,YAAY,CAAC;IAClB,IAAI,EAAE;QACJ,UAAU,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;QAC3C,cAAc,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;KAChD,CAAC;IACF,OAAO,EAAE,gBAAgB,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAClC,YAAY,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CACxC;AAoBD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,CAU1F"}
package/dist/tools.js CHANGED
@@ -1,104 +1,306 @@
1
- import { ResourceTemplate } from '@modelcontextprotocol/server';
2
- import { z, ZodError } from 'zod';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ZodError } from 'zod';
3
+ import { abortableDelay } from './abort.js';
4
+ import { safeCorrelationId } from './correlation.js';
3
5
  import { DataBasinError, publicError } from './errors.js';
4
- import { mapCancel, mapConnections, mapOperation, mapProjects, mapSchema, mapSearch } from './mappers.js';
5
- import { assertSafeOutput, correlationId } from './safety.js';
6
- import { AssistantAuthOutputSchema, BrowseSchemaInputSchema, BrowseSchemaOutputSchema, CancelOperationInputSchema, CancelOperationOutputSchema, ContextOutputSchema, OperationInputSchema, OperationOutputSchema, SearchInputSchema, SearchOutputSchema } from './schemas.js';
7
- export const TOOL_NAMES = Object.freeze([
6
+ import { assertSafeOutput, sanitizeOutput } from './safety.js';
7
+ import { AgentRunEnvelopeSchema, AgentRunLookupInputSchema, AgentRunRequestSchema, AgentRunToolInputSchema, AssistantAuthOutputSchema, AssistantEnvelopeSchema, CapabilitiesEnvelopeSchema, CancelAgentRunInputSchema, ContextEnvelopeSchema, GetSchemaEnvelopeSchema, GetSchemaInputSchema, QueryEnvelopeSchema, QueryLookupInputSchema, RunAssistantInputSchema, RunQueryInputSchema, SearchEnvelopeSchema, SearchInputSchema } from './schemas.js';
8
+ export const LOCAL_AUTH_TOOL_NAMES = Object.freeze([
8
9
  'databasin_auth_status',
9
- 'databasin_login',
10
+ 'databasin_login'
11
+ ]);
12
+ export const REMOTE_PRODUCT_TOOL_NAMES = Object.freeze([
13
+ 'databasin_get_capabilities',
10
14
  'databasin_get_context',
11
15
  'databasin_search',
12
- 'databasin_browse_schema',
13
- 'databasin_get_operation',
14
- 'databasin_cancel_operation'
16
+ 'databasin_get_schema',
17
+ 'databasin_run_agent',
18
+ 'databasin_get_agent_run',
19
+ 'databasin_cancel_agent_run',
20
+ 'databasin_run_query',
21
+ 'databasin_get_query_status',
22
+ 'databasin_cancel_query',
23
+ 'databasin_run_assistant'
24
+ ]);
25
+ export const TOOL_NAMES = Object.freeze([
26
+ ...LOCAL_AUTH_TOOL_NAMES,
27
+ ...REMOTE_PRODUCT_TOOL_NAMES
15
28
  ]);
29
+ const DEFAULT_POLLING = {
30
+ maxWaitMs: 30 * 60 * 1_000,
31
+ initialDelayMs: 500,
32
+ maxDelayMs: 10_000,
33
+ sleep: abortableDelay,
34
+ now: Date.now
35
+ };
36
+ // A query is meant to feel like a fast synchronous tool call, not a long-running
37
+ // job like an agent run -- bounded to 2 minutes, not 30.
38
+ const DEFAULT_QUERY_POLLING = {
39
+ maxWaitMs: 120_000,
40
+ initialDelayMs: 500,
41
+ maxDelayMs: 10_000,
42
+ sleep: abortableDelay,
43
+ now: Date.now
44
+ };
16
45
  export function registerDataBasinTools(server, dependencies) {
17
- register(server, dependencies, 'databasin_auth_status', {
46
+ const polling = { ...DEFAULT_POLLING, ...dependencies.polling };
47
+ const queryPolling = { ...DEFAULT_QUERY_POLLING, ...dependencies.queryPolling };
48
+ registerAuthTools(server, dependencies);
49
+ registerReadTools(server, dependencies);
50
+ registerAgentRunTool(server, dependencies, polling);
51
+ registerAgentStatusTools(server, dependencies);
52
+ registerQueryTool(server, dependencies, queryPolling);
53
+ registerQueryStatusTools(server, dependencies);
54
+ registerAssistantTool(server, dependencies);
55
+ }
56
+ function registerAuthTools(server, dependencies) {
57
+ const authStatus = guardedTool(dependencies, {
18
58
  title: 'Check DataBasin sign-in',
19
- description: 'Check whether this local MCP client is signed in or waiting for browser sign-in. Returns no account identity or credentials.',
20
- inputSchema: z.object({}).strict(),
59
+ description: 'Check local sign-in state without returning account identity or credentials.',
60
+ inputSchema: emptyInputSchema(),
21
61
  outputSchema: AssistantAuthOutputSchema,
22
62
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
23
- }, async () => dependencies.auth.getLoginStatus());
24
- register(server, dependencies, 'databasin_login', {
63
+ }, async () => projectAuthOutput(await dependencies.auth.getLoginStatus()));
64
+ server.registerTool('databasin_auth_status', authStatus.definition, authStatus.callback);
65
+ const login = guardedTool(dependencies, {
25
66
  title: 'Sign in to DataBasin',
26
- description: 'Start Microsoft device sign-in and return a short-lived URL and user code usable on any device. Credentials must be entered only on Microsoft\'s page.',
27
- inputSchema: z.object({}).strict(),
67
+ description: 'Start Microsoft device sign-in. Enter credentials only on Microsoft\'s page; never provide credentials in chat.',
68
+ inputSchema: emptyInputSchema(),
28
69
  outputSchema: AssistantAuthOutputSchema,
29
70
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
30
- }, async () => dependencies.auth.startLogin());
31
- register(server, dependencies, 'databasin_get_context', {
71
+ }, async () => projectAuthOutput(await dependencies.auth.startLogin()));
72
+ server.registerTool('databasin_login', login.definition, login.callback);
73
+ }
74
+ function projectAuthOutput(raw) {
75
+ const output = AssistantAuthOutputSchema.parse(sanitizeOutput(raw));
76
+ return { ...output, message: authOutputMessage(output) };
77
+ }
78
+ function authOutputMessage(output) {
79
+ if (output.status === 'pending') {
80
+ return output.loginUrl === null
81
+ ? 'Microsoft is preparing the device sign-in instructions.'
82
+ : 'Microsoft device sign-in is ready. Use the approved login URL and user code, then call databasin_auth_status.';
83
+ }
84
+ if (output.status === 'authenticated')
85
+ return 'DataBasin sign-in is complete.';
86
+ if (output.status === 'not_authenticated')
87
+ return 'DataBasin is not signed in. Call databasin_login to begin.';
88
+ return 'DataBasin sign-in did not complete. Call databasin_login to try again.';
89
+ }
90
+ function registerReadTools(server, dependencies) {
91
+ const capabilities = guardedTool(dependencies, {
92
+ title: 'Get DataBasin capabilities',
93
+ description: 'Fetch the server-owned allowlist before using other DataBasin capabilities.',
94
+ inputSchema: emptyInputSchema(),
95
+ outputSchema: CapabilitiesEnvelopeSchema,
96
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
97
+ }, async (_input, signal) => dependencies.api.getCapabilities(signal));
98
+ server.registerTool('databasin_get_capabilities', capabilities.definition, capabilities.callback);
99
+ const context = guardedTool(dependencies, {
32
100
  title: 'Get DataBasin context',
33
- description: 'Return projects and connection identifiers visible to the signed-in DataBasin user. Call this first after sign-in.',
34
- inputSchema: z.object({}).strict(),
35
- outputSchema: ContextOutputSchema,
101
+ description: 'Enumerate everything the signed-in user has access to: projects, connections (each with name, connector type/subtype, owning institution, and active status), and capabilities.',
102
+ inputSchema: emptyInputSchema(),
103
+ outputSchema: ContextEnvelopeSchema,
36
104
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
37
- }, async (_input, signal) => {
38
- const [projects, connections] = await Promise.all([
39
- dependencies.api.getProjects(signal),
40
- dependencies.api.getAccessibleConnections(signal)
41
- ]);
42
- return ContextOutputSchema.parse({
43
- environment: dependencies.profile.environment,
44
- projects: mapProjects(projects).slice(0, 500),
45
- connections: mapConnections(connections).slice(0, 500),
46
- capabilities: TOOL_NAMES
47
- });
48
- });
49
- register(server, dependencies, 'databasin_search', {
105
+ }, async (_input, signal) => dependencies.api.getContext(signal));
106
+ server.registerTool('databasin_get_context', context.definition, context.callback);
107
+ const search = guardedTool(dependencies, {
50
108
  title: 'Search DataBasin',
51
- description: 'Search visible pipelines, connectors, and automations using the fixed DataBasin API.',
109
+ description: 'Search: a bounded name/type substring match over the signed-in user\'s authorized scope (max 25 results). Not a listing facility -- use databasin_get_context to enumerate everything the caller has access to.',
52
110
  inputSchema: SearchInputSchema,
53
- outputSchema: SearchOutputSchema,
111
+ outputSchema: SearchEnvelopeSchema,
54
112
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
55
- }, async (input, signal) => mapSearch(await dependencies.api.search(input, signal), input.limit));
56
- register(server, dependencies, 'databasin_browse_schema', {
57
- title: 'Browse DataBasin schema',
58
- description: 'Browse bounded catalogs, schemas, tables, or columns for one visible connector.',
59
- inputSchema: BrowseSchemaInputSchema,
60
- outputSchema: BrowseSchemaOutputSchema,
113
+ }, async (input, signal) => dependencies.api.search(input, signal));
114
+ server.registerTool('databasin_search', search.definition, search.callback);
115
+ const schema = guardedTool(dependencies, {
116
+ title: 'Get DataBasin schema metadata',
117
+ description: 'Read bounded non-secret catalogs, schemas, tables, or columns for one authorized connector. A POLICY_DENIED error may include a "reason" field with a specific, safe remediation hint (for example, which access is missing) when the server provides one.',
118
+ inputSchema: GetSchemaInputSchema,
119
+ outputSchema: GetSchemaEnvelopeSchema,
61
120
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
62
- }, async (input, signal) => mapSchema(await dependencies.api.browseSchema(input, signal), input));
63
- register(server, dependencies, 'databasin_get_operation', {
64
- title: 'Get DataBasin query operation',
65
- description: 'Read bounded status and results for a caller-visible DataBasin query operation.',
66
- inputSchema: OperationInputSchema,
67
- outputSchema: OperationOutputSchema,
121
+ }, async (input, signal) => dependencies.api.getSchema(input, signal));
122
+ server.registerTool('databasin_get_schema', schema.definition, schema.callback);
123
+ }
124
+ function registerAgentRunTool(server, dependencies, polling) {
125
+ const runAgent = guardedTool(dependencies, {
126
+ title: 'Run a DataBasin metadata agent',
127
+ description: 'Start the fixed metadata_readonly agent profile for an explicit institution and optional project/connector scope. No arbitrary SQL, commands, URLs, headers, or credentials are accepted. If the run is still not finished after the 30-minute polling limit, the last observed non-terminal run (for example status "running") is returned with its runId; use databasin_get_agent_run or databasin_cancel_agent_run to follow up.',
128
+ inputSchema: AgentRunToolInputSchema,
129
+ outputSchema: AgentRunEnvelopeSchema,
130
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
131
+ }, async (input, signal) => {
132
+ const { idempotencyKey, ...rawRequest } = input;
133
+ const request = AgentRunRequestSchema.parse(rawRequest);
134
+ const created = await dependencies.api.createAgentRun(request, idempotencyKey ?? randomUUID(), signal);
135
+ if (isTerminal(created.data.status))
136
+ return created;
137
+ try {
138
+ return await pollAgentRun(dependencies.api, created, signal, polling);
139
+ }
140
+ catch (error) {
141
+ if (isCallerAbort(error, signal))
142
+ await requestCancellation(dependencies.api, created.data.runId);
143
+ throw error;
144
+ }
145
+ });
146
+ server.registerTool('databasin_run_agent', runAgent.definition, runAgent.callback);
147
+ }
148
+ function registerAgentStatusTools(server, dependencies) {
149
+ const getRun = guardedTool(dependencies, {
150
+ title: 'Get a DataBasin agent run',
151
+ description: 'Read the current bounded status/result and complete server-authorized scope of an agent run.',
152
+ inputSchema: AgentRunLookupInputSchema,
153
+ outputSchema: AgentRunEnvelopeSchema,
68
154
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
69
- }, async (input, signal) => mapOperation(await dependencies.api.getOperation(input.operationId, { offset: input.offset, limit: input.limit }, signal), input.operationId, { offset: input.offset, limit: input.limit }));
70
- register(server, dependencies, 'databasin_cancel_operation', {
71
- title: 'Cancel DataBasin query operation',
72
- description: 'Request cancellation of a caller-visible DataBasin query operation. Cancellation is re-authorized by the API.',
73
- inputSchema: CancelOperationInputSchema,
74
- outputSchema: CancelOperationOutputSchema,
155
+ }, async (input, signal) => dependencies.api.getAgentRun(input.runId, signal));
156
+ server.registerTool('databasin_get_agent_run', getRun.definition, getRun.callback);
157
+ const cancelRun = guardedTool(dependencies, {
158
+ title: 'Cancel a DataBasin agent run',
159
+ description: 'Request cancellation of an authorized agent run. A running provider may remain running until it acknowledges cancellation; this tool never claims cancellation before the server reports it.',
160
+ inputSchema: CancelAgentRunInputSchema,
161
+ outputSchema: AgentRunEnvelopeSchema,
75
162
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
76
- }, async (input, signal) => mapCancel(await dependencies.api.cancelOperation(input.operationId, signal), input.operationId));
77
- server.registerResource('databasin-operation', new ResourceTemplate('databasin://operations/{operationId}', { list: undefined }), {
78
- title: 'DataBasin operation result',
79
- description: 'Bounded, re-authorized DataBasin operation status and first result page.',
80
- mimeType: 'application/json',
81
- cacheHint: { cacheScope: 'private', ttlMs: 0 }
82
- }, async (uri, variables) => {
163
+ }, async (input, signal) => dependencies.api.cancelAgentRun(input.runId, input.idempotencyKey ?? randomUUID(), input.reason, signal));
164
+ server.registerTool('databasin_cancel_agent_run', cancelRun.definition, cancelRun.callback);
165
+ }
166
+ function registerQueryTool(server, dependencies, polling) {
167
+ const runQuery = guardedTool(dependencies, {
168
+ title: 'Run a read-only DataBasin SQL query',
169
+ description: 'Execute a single read-only SELECT or WITH statement against an authorized connector and return up to maxRows rows (default and hard max 1000). No INSERT, UPDATE, DELETE, DDL, or other mutating statements are accepted, and multi-statement text is rejected outright. If the query is still not finished after the 2-minute polling limit, the last observed non-terminal status (for example "running") is returned with its queryId; use databasin_get_query_status or databasin_cancel_query to follow up.',
170
+ inputSchema: RunQueryInputSchema,
171
+ outputSchema: QueryEnvelopeSchema,
172
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
173
+ }, async (input, signal) => {
174
+ const created = await dependencies.api.runQuery(input, signal);
175
+ if (isQueryTerminal(created.data.status))
176
+ return created;
83
177
  try {
84
- const operationId = String(variables.operationId ?? '');
85
- if (!operationId || operationId.length > 256)
86
- throw new DataBasinError('INVALID_ARGUMENT', 'A valid operation ID is required.');
87
- const output = mapOperation(await dependencies.api.getOperation(operationId, { offset: 0, limit: 100 }), operationId, { offset: 0, limit: 100 });
88
- assertSafeOutput(output, dependencies.maxOutputBytes);
89
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(output) }] };
178
+ return await pollQuery(dependencies.api, created, signal, polling);
90
179
  }
91
180
  catch (error) {
92
- const safe = { ...publicError(error), correlationId: correlationId() };
93
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(safe) }] };
181
+ if (isCallerAbort(error, signal))
182
+ await requestQueryCancellation(dependencies.api, created.data.queryId);
183
+ throw error;
94
184
  }
95
185
  });
186
+ server.registerTool('databasin_run_query', runQuery.definition, runQuery.callback);
187
+ }
188
+ function registerQueryStatusTools(server, dependencies) {
189
+ const getStatus = guardedTool(dependencies, {
190
+ title: 'Get a DataBasin query status',
191
+ description: 'Read the current bounded status of a query started by databasin_run_query, including its columns, rows (capped at the run\'s maxRows), and error once the query reaches a terminal status.',
192
+ inputSchema: QueryLookupInputSchema,
193
+ outputSchema: QueryEnvelopeSchema,
194
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
195
+ }, async (input, signal) => dependencies.api.getQueryStatus(input.queryId, signal));
196
+ server.registerTool('databasin_get_query_status', getStatus.definition, getStatus.callback);
197
+ const cancelQuery = guardedTool(dependencies, {
198
+ title: 'Cancel a DataBasin query',
199
+ description: 'Request cancellation of an authorized, in-progress query. A running engine may remain running until it acknowledges cancellation; this tool never claims cancellation before the server reports it.',
200
+ inputSchema: QueryLookupInputSchema,
201
+ outputSchema: QueryEnvelopeSchema,
202
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
203
+ }, async (input, signal) => dependencies.api.cancelQuery(input.queryId, signal));
204
+ server.registerTool('databasin_cancel_query', cancelQuery.definition, cancelQuery.callback);
205
+ }
206
+ function registerAssistantTool(server, dependencies) {
207
+ const runAssistant = guardedTool(dependencies, {
208
+ title: 'Run the DataBasin assistant',
209
+ description: 'A read-only DataBasin assistant (a server-side Claude Code agent) that can browse metadata and run DataBasin skills over multiple steps to answer a natural-language request. Requires an LLM connectorId. Read-only: no data writes and no SQL execution -- use databasin_run_query for SQL. Synchronous, and may take up to a few minutes to return.',
210
+ inputSchema: RunAssistantInputSchema,
211
+ outputSchema: AssistantEnvelopeSchema,
212
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
213
+ }, async (input, signal) => dependencies.api.runAssistant(input, signal));
214
+ server.registerTool('databasin_run_assistant', runAssistant.definition, runAssistant.callback);
96
215
  }
97
- function register(server, dependencies, name, definition, handler) {
98
- server.registerTool(name, definition, async (rawInput, context) => {
216
+ async function pollAgentRun(api, initial, signal, options) {
217
+ let current = initial;
218
+ let delay = options.initialDelayMs;
219
+ const deadline = options.now() + options.maxWaitMs;
220
+ while (!isTerminal(current.data.status)) {
221
+ if (signal?.aborted)
222
+ throw new DataBasinError('REQUEST_ABORTED', 'The DataBasin request was cancelled by the caller.');
223
+ const remaining = deadline - options.now();
224
+ if (remaining <= 0)
225
+ return current;
226
+ try {
227
+ await options.sleep(Math.min(delay, remaining), signal);
228
+ }
229
+ catch (error) {
230
+ if (signal?.aborted) {
231
+ throw new DataBasinError('REQUEST_ABORTED', 'The DataBasin request was cancelled by the caller.');
232
+ }
233
+ throw error;
234
+ }
235
+ current = await api.getAgentRun(current.data.runId, signal);
236
+ delay = Math.min(delay * 2, options.maxDelayMs);
237
+ }
238
+ return current;
239
+ }
240
+ async function requestCancellation(api, runId) {
241
+ const controller = new AbortController();
242
+ const timeout = setTimeout(() => controller.abort(), 5_000);
243
+ try {
244
+ await api.cancelAgentRun(runId, randomUUID(), 'user_requested', controller.signal);
245
+ }
246
+ catch {
247
+ // The caller's cancellation remains authoritative; do not claim a server state.
248
+ }
249
+ finally {
250
+ clearTimeout(timeout);
251
+ }
252
+ }
253
+ function isTerminal(status) {
254
+ return status === 'completed' || status === 'failed' || status === 'cancelled';
255
+ }
256
+ async function pollQuery(api, initial, signal, options) {
257
+ let current = initial;
258
+ let delay = options.initialDelayMs;
259
+ const deadline = options.now() + options.maxWaitMs;
260
+ while (!isQueryTerminal(current.data.status)) {
261
+ if (signal?.aborted)
262
+ throw new DataBasinError('REQUEST_ABORTED', 'The DataBasin request was cancelled by the caller.');
263
+ const remaining = deadline - options.now();
264
+ if (remaining <= 0)
265
+ return current;
266
+ try {
267
+ await options.sleep(Math.min(delay, remaining), signal);
268
+ }
269
+ catch (error) {
270
+ if (signal?.aborted) {
271
+ throw new DataBasinError('REQUEST_ABORTED', 'The DataBasin request was cancelled by the caller.');
272
+ }
273
+ throw error;
274
+ }
275
+ current = await api.getQueryStatus(current.data.queryId, signal);
276
+ delay = Math.min(delay * 2, options.maxDelayMs);
277
+ }
278
+ return current;
279
+ }
280
+ async function requestQueryCancellation(api, queryId) {
281
+ const controller = new AbortController();
282
+ const timeout = setTimeout(() => controller.abort(), 5_000);
283
+ try {
284
+ await api.cancelQuery(queryId, controller.signal);
285
+ }
286
+ catch {
287
+ // The caller's cancellation remains authoritative; do not claim a server state.
288
+ }
289
+ finally {
290
+ clearTimeout(timeout);
291
+ }
292
+ }
293
+ function isQueryTerminal(status) {
294
+ return status === 'completed' || status === 'failed' || status === 'cancelled';
295
+ }
296
+ function isCallerAbort(error, signal) {
297
+ return signal?.aborted === true || error instanceof DataBasinError && error.code === 'REQUEST_ABORTED';
298
+ }
299
+ function guardedTool(dependencies, definition, handler) {
300
+ const callback = async (rawInput, context) => {
99
301
  try {
100
302
  const input = definition.inputSchema.parse(rawInput ?? {});
101
- const output = definition.outputSchema.parse(await handler(input, context.mcpReq.signal));
303
+ const output = definition.outputSchema.parse(sanitizeOutput(await handler(input, context.mcpReq.signal)));
102
304
  assertSafeOutput(output, dependencies.maxOutputBytes);
103
305
  return {
104
306
  content: [{ type: 'text', text: JSON.stringify(output) }],
@@ -109,9 +311,18 @@ function register(server, dependencies, name, definition, handler) {
109
311
  const normalized = error instanceof ZodError
110
312
  ? new DataBasinError('INVALID_ARGUMENT', error.issues[0]?.message ?? 'The tool arguments are invalid.')
111
313
  : error;
112
- const safe = { ...publicError(normalized), correlationId: correlationId() };
314
+ const visible = publicError(normalized);
315
+ const { correlationId: upstreamCorrelation, ...publicFields } = visible;
316
+ const safe = {
317
+ ...publicFields,
318
+ correlationId: safeCorrelationId(upstreamCorrelation)
319
+ };
113
320
  return { content: [{ type: 'text', text: JSON.stringify(safe) }], isError: true };
114
321
  }
115
- });
322
+ };
323
+ return Object.freeze({ definition, callback });
324
+ }
325
+ function emptyInputSchema() {
326
+ return AgentRunLookupInputSchema.pick({}).strict();
116
327
  }
117
328
  //# sourceMappingURL=tools.js.map