@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.
- package/README.md +128 -61
- package/dist/abort.d.ts +5 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +25 -0
- package/dist/abort.js.map +1 -0
- package/dist/api-client.d.ts +49 -34
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +142 -145
- package/dist/api-client.js.map +1 -1
- package/dist/api-contract.d.ts +59 -0
- package/dist/api-contract.d.ts.map +1 -0
- package/dist/api-contract.js +47 -0
- package/dist/api-contract.js.map +1 -0
- package/dist/api-profile.d.ts +5 -0
- package/dist/api-profile.d.ts.map +1 -0
- package/dist/api-profile.js +38 -0
- package/dist/api-profile.js.map +1 -0
- package/dist/api-request.d.ts +27 -0
- package/dist/api-request.d.ts.map +1 -0
- package/dist/api-request.js +147 -0
- package/dist/api-request.js.map +1 -0
- package/dist/api-response.d.ts +9 -0
- package/dist/api-response.d.ts.map +1 -0
- package/dist/api-response.js +206 -0
- package/dist/api-response.js.map +1 -0
- package/dist/auth.d.ts +22 -11
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +71 -71
- package/dist/auth.js.map +1 -1
- package/dist/browser.d.ts +20 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +19 -17
- package/dist/browser.js.map +1 -1
- package/dist/cli.d.ts +31 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +147 -67
- package/dist/cli.js.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/correlation.d.ts +6 -0
- package/dist/correlation.d.ts.map +1 -0
- package/dist/correlation.js +15 -0
- package/dist/correlation.js.map +1 -0
- package/dist/credential-store.d.ts +21 -0
- package/dist/credential-store.d.ts.map +1 -0
- package/dist/credential-store.js +46 -0
- package/dist/credential-store.js.map +1 -0
- package/dist/errors.d.ts +17 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +24 -6
- package/dist/errors.js.map +1 -1
- package/dist/login-url.d.ts +18 -0
- package/dist/login-url.d.ts.map +1 -0
- package/dist/login-url.js +82 -0
- package/dist/login-url.js.map +1 -0
- package/dist/safety.d.ts +4 -0
- package/dist/safety.d.ts.map +1 -1
- package/dist/safety.js +94 -10
- package/dist/safety.js.map +1 -1
- package/dist/schemas.d.ts +326 -49
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +242 -55
- package/dist/schemas.js.map +1 -1
- package/dist/server.d.ts +2 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +17 -10
- package/dist/server.js.map +1 -1
- package/dist/token-validation.d.ts +5 -0
- package/dist/token-validation.d.ts.map +1 -0
- package/dist/token-validation.js +183 -0
- package/dist/token-validation.js.map +1 -0
- package/dist/tools.d.ts +12 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +286 -75
- package/dist/tools.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +10 -6
- package/dist/mappers.d.ts +0 -56
- package/dist/mappers.d.ts.map +0 -1
- package/dist/mappers.js +0 -171
- 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
|
|
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 {};
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
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
|
-
|
|
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
|
|
20
|
-
inputSchema:
|
|
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
|
-
|
|
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
|
|
27
|
-
inputSchema:
|
|
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
|
-
|
|
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: '
|
|
34
|
-
inputSchema:
|
|
35
|
-
outputSchema:
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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:
|
|
111
|
+
outputSchema: SearchEnvelopeSchema,
|
|
54
112
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
55
|
-
}, async (input, signal) =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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) =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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) =>
|
|
77
|
-
server.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
98
|
-
|
|
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
|
|
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
|