@hypequery/mcp 0.9.0 → 0.10.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 +70 -0
- package/dist/executor.d.ts +3 -3
- package/dist/executor.d.ts.map +1 -1
- package/dist/http-executor.d.ts +40 -0
- package/dist/http-executor.d.ts.map +1 -0
- package/dist/http-executor.js +178 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/protocol-server.js +3 -3
- package/dist/tools/args.d.ts +65 -123
- package/dist/tools/args.d.ts.map +1 -1
- package/dist/tools/args.js +3 -1
- package/dist/tools/tool-manifest.d.ts +1 -1
- package/dist/tools/tool-manifest.d.ts.map +1 -1
- package/dist/tools/tool-manifest.js +1 -47
- package/dist/tools/utils/canonical-query-schemas.d.ts +1 -1
- package/dist/tools/utils/canonical-query-schemas.d.ts.map +1 -1
- package/dist/tools/utils/canonical-query-schemas.js +1 -1
- package/dist/tools/utils/result-schema.d.ts +5 -0
- package/dist/tools/utils/result-schema.d.ts.map +1 -0
- package/dist/tools/utils/result-schema.js +47 -0
- package/dist/utils/http-client/arguments.d.ts +3 -0
- package/dist/utils/http-client/arguments.d.ts.map +1 -0
- package/dist/utils/http-client/arguments.js +26 -0
- package/dist/utils/http-client/discovery.d.ts +6 -0
- package/dist/utils/http-client/discovery.d.ts.map +1 -0
- package/dist/utils/http-client/discovery.js +24 -0
- package/dist/utils/http-client/json.d.ts +6 -0
- package/dist/utils/http-client/json.d.ts.map +1 -0
- package/dist/utils/http-client/json.js +79 -0
- package/dist/utils/http-client/manifest.d.ts +5 -0
- package/dist/utils/http-client/manifest.d.ts.map +1 -0
- package/dist/utils/http-client/manifest.js +53 -0
- package/dist/utils/http-client/profile.d.ts +5 -0
- package/dist/utils/http-client/profile.d.ts.map +1 -0
- package/dist/utils/http-client/profile.js +29 -0
- package/dist/utils/http-client/result.d.ts +4 -0
- package/dist/utils/http-client/result.d.ts.map +1 -0
- package/dist/utils/http-client/result.js +98 -0
- package/dist/utils/http-client/transport.d.ts +12 -0
- package/dist/utils/http-client/transport.d.ts.map +1 -0
- package/dist/utils/http-client/transport.js +88 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -6
- package/dist/api.type-test.d.ts +0 -2
- package/dist/api.type-test.d.ts.map +0 -1
- package/dist/api.type-test.js +0 -31
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SUPPORTED_TIME_GRAINS } from '@hypequery/datasets';
|
|
2
|
+
import { resultSchema } from './utils/result-schema.js';
|
|
2
3
|
const semanticMetadataProperties = {
|
|
3
4
|
examples: { type: 'array', items: { type: 'string' } },
|
|
4
5
|
synonyms: { type: 'array', items: { type: 'string' } },
|
|
@@ -10,53 +11,6 @@ const semanticMetadataProperties = {
|
|
|
10
11
|
enum: ['public', 'internal', 'confidential', 'restricted'],
|
|
11
12
|
},
|
|
12
13
|
};
|
|
13
|
-
const errorEnvelopeSchema = {
|
|
14
|
-
type: 'object',
|
|
15
|
-
additionalProperties: false,
|
|
16
|
-
properties: {
|
|
17
|
-
error: {
|
|
18
|
-
type: 'object',
|
|
19
|
-
additionalProperties: false,
|
|
20
|
-
properties: {
|
|
21
|
-
code: {
|
|
22
|
-
type: 'string',
|
|
23
|
-
enum: [
|
|
24
|
-
'MCP_INVALID_ARGUMENTS',
|
|
25
|
-
'MCP_NOT_FOUND',
|
|
26
|
-
'MCP_UNKNOWN_TOOL',
|
|
27
|
-
'MCP_UNAUTHORIZED',
|
|
28
|
-
'MCP_STALE_CONTRACT',
|
|
29
|
-
'MCP_REQUEST_CANCELLED',
|
|
30
|
-
'MCP_QUERY_TIMEOUT',
|
|
31
|
-
'MCP_RESULT_TOO_LARGE',
|
|
32
|
-
'MCP_EXECUTION_FAILED',
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
message: { type: 'string' },
|
|
36
|
-
category: {
|
|
37
|
-
type: 'string',
|
|
38
|
-
enum: [
|
|
39
|
-
'correctable_input',
|
|
40
|
-
'unauthorized',
|
|
41
|
-
'stale_contract',
|
|
42
|
-
'budget',
|
|
43
|
-
'internal',
|
|
44
|
-
],
|
|
45
|
-
},
|
|
46
|
-
retryable: { type: 'boolean' },
|
|
47
|
-
correctable: { type: 'boolean' },
|
|
48
|
-
},
|
|
49
|
-
required: ['code', 'category', 'message', 'retryable', 'correctable'],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
required: ['error'],
|
|
53
|
-
};
|
|
54
|
-
function resultSchema(successSchema) {
|
|
55
|
-
return {
|
|
56
|
-
type: 'object',
|
|
57
|
-
oneOf: [successSchema, errorEnvelopeSchema],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
14
|
export const DATASET_LIST_OUTPUT_SCHEMA = resultSchema({
|
|
61
15
|
type: 'object',
|
|
62
16
|
additionalProperties: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CanonicalSemanticQuerySchemas } from '@hypequery/datasets';
|
|
1
|
+
import { type CanonicalSemanticQuerySchemas } from '@hypequery/datasets/schemas';
|
|
2
2
|
import type { DatasetRegistry, MCPQueryLimits } from '../../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Compile exact schemas for real Dataset registries. The shipped loose registry
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canonical-query-schemas.d.ts","sourceRoot":"","sources":["../../../src/tools/utils/canonical-query-schemas.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"canonical-query-schemas.d.ts","sourceRoot":"","sources":["../../../src/tools/utils/canonical-query-schemas.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AA0BtE;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,eAAe,EACzB,UAAU,CAAC,EAAE,cAAc;AAC3B;;;;;;GAMG;AACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE;AACrC;;;;GAIG;AACH,QAAQ,GAAE;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAqB,GACzD,6BAA6B,CAsG/B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { buildCanonicalSemanticQuerySchemas, toSemanticJsonSchema, } from '@hypequery/datasets';
|
|
2
|
+
import { buildCanonicalSemanticQuerySchemas, toSemanticJsonSchema, } from '@hypequery/datasets/schemas';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { queryDatasetArgsSchema, queryMetricArgsSchema } from '../args.js';
|
|
5
5
|
import { resolveQueryLimits } from './query-limits.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
type ObjectSchema = Tool['outputSchema'] & Record<string, unknown>;
|
|
3
|
+
export declare function resultSchema(successSchema: Record<string, unknown>): ObjectSchema;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=result-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result-schema.d.ts","sourceRoot":"","sources":["../../../src/tools/utils/result-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE/D,KAAK,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA4CnE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,CAKjF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const errorEnvelopeSchema = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
additionalProperties: false,
|
|
4
|
+
properties: {
|
|
5
|
+
error: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
additionalProperties: false,
|
|
8
|
+
properties: {
|
|
9
|
+
code: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
enum: [
|
|
12
|
+
'MCP_INVALID_ARGUMENTS',
|
|
13
|
+
'MCP_NOT_FOUND',
|
|
14
|
+
'MCP_UNKNOWN_TOOL',
|
|
15
|
+
'MCP_UNAUTHORIZED',
|
|
16
|
+
'MCP_STALE_CONTRACT',
|
|
17
|
+
'MCP_REQUEST_CANCELLED',
|
|
18
|
+
'MCP_QUERY_TIMEOUT',
|
|
19
|
+
'MCP_RESULT_TOO_LARGE',
|
|
20
|
+
'MCP_EXECUTION_FAILED',
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
message: { type: 'string' },
|
|
24
|
+
category: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: [
|
|
27
|
+
'correctable_input',
|
|
28
|
+
'unauthorized',
|
|
29
|
+
'stale_contract',
|
|
30
|
+
'budget',
|
|
31
|
+
'internal',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
retryable: { type: 'boolean' },
|
|
35
|
+
correctable: { type: 'boolean' },
|
|
36
|
+
},
|
|
37
|
+
required: ['code', 'category', 'message', 'retryable', 'correctable'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ['error'],
|
|
41
|
+
};
|
|
42
|
+
export function resultSchema(successSchema) {
|
|
43
|
+
return {
|
|
44
|
+
type: 'object',
|
|
45
|
+
oneOf: [successSchema, errorEnvelopeSchema],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arguments.d.ts","sourceRoot":"","sources":["../../../src/utils/http-client/arguments.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBrG"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MCPToolError } from '../../errors.js';
|
|
2
|
+
/** Read only own data properties. Nested query values go through the shared bounded validator. */
|
|
3
|
+
export function httpToolArguments(input, allowed) {
|
|
4
|
+
try {
|
|
5
|
+
const value = input === undefined ? {} : input;
|
|
6
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
throw new Error();
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
10
|
+
throw new Error();
|
|
11
|
+
const keys = Reflect.ownKeys(value);
|
|
12
|
+
if (keys.length > allowed.length || keys.some(key => typeof key !== 'string' || !allowed.includes(key)))
|
|
13
|
+
throw new Error();
|
|
14
|
+
const result = Object.create(null);
|
|
15
|
+
for (const key of keys) {
|
|
16
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
17
|
+
if (!descriptor?.enumerable || !('value' in descriptor))
|
|
18
|
+
throw new Error();
|
|
19
|
+
result[key] = descriptor.value;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new MCPToolError('MCP_INVALID_ARGUMENTS', 'Invalid client tool arguments.');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProtocolClientContract } from '@hypequery/protocol';
|
|
2
|
+
export declare function clientDatasetNames(contract: ProtocolClientContract): string[];
|
|
3
|
+
/** Preserve endpoint-specific permissions instead of merging them into a synthetic dataset. */
|
|
4
|
+
export declare function clientDatasetProfile(contract: ProtocolClientContract, dataset: string): Record<string, unknown>;
|
|
5
|
+
export declare function clientDatasetList(contract: ProtocolClientContract): Record<string, unknown>;
|
|
6
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/utils/http-client/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAElE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,sBAAsB,GAAG,MAAM,EAAE,CAE7E;AAED,+FAA+F;AAC/F,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAM/G;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAY3F"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function clientDatasetNames(contract) {
|
|
2
|
+
return [...new Set(contract.endpoints.map(endpoint => endpoint.dataset))].sort();
|
|
3
|
+
}
|
|
4
|
+
/** Preserve endpoint-specific permissions instead of merging them into a synthetic dataset. */
|
|
5
|
+
export function clientDatasetProfile(contract, dataset) {
|
|
6
|
+
const endpoints = contract.endpoints.filter(endpoint => endpoint.dataset === dataset);
|
|
7
|
+
return {
|
|
8
|
+
name: dataset, queryable: endpoints.some(endpoint => endpoint.kind === 'dataset'),
|
|
9
|
+
endpoints: endpoints.map(({ path: _path, schemaHash: _hash, ...profile }) => profile),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function clientDatasetList(contract) {
|
|
13
|
+
const datasets = clientDatasetNames(contract).map(name => {
|
|
14
|
+
const endpoints = contract.endpoints.filter(endpoint => endpoint.dataset === name);
|
|
15
|
+
return {
|
|
16
|
+
name, description: 'Authorized client endpoints.',
|
|
17
|
+
queryable: endpoints.some(endpoint => endpoint.kind === 'dataset'),
|
|
18
|
+
dimensionCount: new Set(endpoints.flatMap(endpoint => Object.keys(endpoint.dimensions))).size,
|
|
19
|
+
measureCount: new Set(endpoints.flatMap(endpoint => Object.keys(endpoint.measures))).size,
|
|
20
|
+
metricCount: endpoints.filter(endpoint => endpoint.kind === 'metric').length,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
return { datasets, total: datasets.length };
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare class HttpResultJsonBudgetError extends Error {
|
|
2
|
+
constructor();
|
|
3
|
+
}
|
|
4
|
+
/** Budget/duplicate scan before native JSON parsing. Scalars retain HTTP binary64 semantics, including -0. */
|
|
5
|
+
export declare function decodeHttpResultJson(text: string): unknown;
|
|
6
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../../src/utils/http-client/json.ts"],"names":[],"mappings":"AAAA,qBAAa,yBAA0B,SAAQ,KAAK;;CAEnD;AAED,8GAA8G;AAC9G,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAoD1D"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export class HttpResultJsonBudgetError extends Error {
|
|
2
|
+
constructor() { super('HTTP result JSON exceeds validation limits.'); }
|
|
3
|
+
}
|
|
4
|
+
/** Budget/duplicate scan before native JSON parsing. Scalars retain HTTP binary64 semantics, including -0. */
|
|
5
|
+
export function decodeHttpResultJson(text) {
|
|
6
|
+
if (Buffer.byteLength(text) > 1_048_576)
|
|
7
|
+
throw new HttpResultJsonBudgetError();
|
|
8
|
+
const stack = [];
|
|
9
|
+
let nodes = 0;
|
|
10
|
+
let position = 0;
|
|
11
|
+
const value = () => {
|
|
12
|
+
if (++nodes > 10_000)
|
|
13
|
+
throw new HttpResultJsonBudgetError();
|
|
14
|
+
const parent = stack[stack.length - 1];
|
|
15
|
+
if (parent && !parent.object && ++parent.count > 1000)
|
|
16
|
+
throw new HttpResultJsonBudgetError();
|
|
17
|
+
};
|
|
18
|
+
while (position < text.length) {
|
|
19
|
+
const character = text[position];
|
|
20
|
+
if (' \t\r\n:'.includes(character)) {
|
|
21
|
+
position++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (character === ',') {
|
|
25
|
+
const parent = stack[stack.length - 1];
|
|
26
|
+
if (parent?.object)
|
|
27
|
+
parent.key = true;
|
|
28
|
+
position++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (character === '{' || character === '[') {
|
|
32
|
+
value();
|
|
33
|
+
if (stack.length >= 16)
|
|
34
|
+
throw new HttpResultJsonBudgetError();
|
|
35
|
+
stack.push({ object: character === '{', keys: new Set(), key: character === '{', count: 0 });
|
|
36
|
+
position++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (character === '}' || character === ']') {
|
|
40
|
+
stack.pop();
|
|
41
|
+
position++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (character === '"') {
|
|
45
|
+
const start = position++;
|
|
46
|
+
let closed = false;
|
|
47
|
+
while (position < text.length) {
|
|
48
|
+
const current = text[position++];
|
|
49
|
+
if (current === '\\')
|
|
50
|
+
position++;
|
|
51
|
+
else if (current === '"') {
|
|
52
|
+
closed = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!closed)
|
|
57
|
+
throw new Error('Invalid JSON string');
|
|
58
|
+
const parent = stack[stack.length - 1];
|
|
59
|
+
if (parent?.object && parent.key) {
|
|
60
|
+
const key = JSON.parse(text.slice(start, position));
|
|
61
|
+
if (parent.keys.has(key))
|
|
62
|
+
throw new Error('Duplicate JSON member');
|
|
63
|
+
parent.keys.add(key);
|
|
64
|
+
if (parent.keys.size > 1000)
|
|
65
|
+
throw new HttpResultJsonBudgetError();
|
|
66
|
+
parent.key = false;
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
value();
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
value();
|
|
73
|
+
position++;
|
|
74
|
+
while (position < text.length && !' \t\r\n,]}:{["'.includes(text[position]))
|
|
75
|
+
position++;
|
|
76
|
+
}
|
|
77
|
+
// Native parsing owns JSON grammar validation; the scan only adds budgets and duplicate refusal.
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ClientContractQuerySchemas } from '@hypequery/datasets';
|
|
2
|
+
import type { ListToolsResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
/** Fixed tool names, canonical client input schemas, and the actual public HTTP result envelope. */
|
|
4
|
+
export declare function clientHttpManifest(schemas: ClientContractQuerySchemas, authorizedHash: string): ListToolsResult;
|
|
5
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/utils/http-client/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAkC1E,oGAAoG;AACpG,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,0BAA0B,EAAE,cAAc,EAAE,MAAM,GAAG,eAAe,CAiB/G"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { buildMCPToolManifest } from '../../tools/tool-manifest.js';
|
|
2
|
+
import { resultSchema } from '../../tools/utils/result-schema.js';
|
|
3
|
+
const profileOutput = resultSchema({
|
|
4
|
+
type: 'object', additionalProperties: false, required: ['name', 'queryable', 'endpoints'],
|
|
5
|
+
properties: { name: { type: 'string' }, queryable: { type: 'boolean' }, endpoints: { type: 'array', items: { type: 'object' } } },
|
|
6
|
+
});
|
|
7
|
+
const listOutput = resultSchema({
|
|
8
|
+
type: 'object', additionalProperties: false, required: ['datasets', 'total'],
|
|
9
|
+
properties: {
|
|
10
|
+
datasets: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
11
|
+
required: ['name', 'description', 'queryable', 'dimensionCount', 'measureCount', 'metricCount'],
|
|
12
|
+
properties: { name: { type: 'string' }, description: { type: 'string' }, queryable: { type: 'boolean' },
|
|
13
|
+
dimensionCount: { type: 'integer', minimum: 0 }, measureCount: { type: 'integer', minimum: 0 }, metricCount: { type: 'integer', minimum: 0 } },
|
|
14
|
+
} }, total: { type: 'integer', minimum: 0 },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const queryOutput = resultSchema({
|
|
18
|
+
type: 'object', additionalProperties: false, required: ['data'],
|
|
19
|
+
properties: {
|
|
20
|
+
data: { type: 'array', maxItems: 1000, items: { type: 'object', additionalProperties: { type: ['string', 'number', 'boolean', 'null'] } } },
|
|
21
|
+
meta: { type: ['object', 'null'], additionalProperties: false,
|
|
22
|
+
required: ['requestId', 'timingMs', 'rowCount', 'pagination'],
|
|
23
|
+
properties: {
|
|
24
|
+
requestId: { type: 'string' }, timingMs: { type: 'number', minimum: 0 }, rowCount: { type: 'integer', minimum: 0 },
|
|
25
|
+
pagination: { type: 'object', additionalProperties: false, required: ['limit', 'offset', 'hasMore'],
|
|
26
|
+
properties: { limit: { type: 'integer', minimum: 1 }, offset: { type: 'integer', minimum: 0 }, hasMore: { type: 'boolean' } } },
|
|
27
|
+
cache: { type: 'object', additionalProperties: false, required: ['hit'], properties: { hit: { type: 'boolean' } } },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
/** Fixed tool names, canonical client input schemas, and the actual public HTTP result envelope. */
|
|
33
|
+
export function clientHttpManifest(schemas, authorizedHash) {
|
|
34
|
+
const manifest = buildMCPToolManifest(schemas);
|
|
35
|
+
for (const tool of manifest.tools) {
|
|
36
|
+
if (tool.name === 'get_dataset_schema') {
|
|
37
|
+
tool.description = 'Inspect authorized endpoint profiles for a dataset group. Each endpoint retains its own permissions and defaults.';
|
|
38
|
+
tool.inputSchema = { type: 'object', additionalProperties: false, properties: { dataset: { type: 'string' } }, required: ['dataset'] };
|
|
39
|
+
tool.outputSchema = profileOutput;
|
|
40
|
+
}
|
|
41
|
+
else if (tool.name === 'list_datasets') {
|
|
42
|
+
tool.description = 'List authorized dataset groups, including metric-only groups whose queryable flag is false.';
|
|
43
|
+
tool.inputSchema = { type: 'object', additionalProperties: false, properties: {} };
|
|
44
|
+
tool.outputSchema = listOutput;
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
tool.outputSchema = queryOutput;
|
|
48
|
+
}
|
|
49
|
+
return { ...manifest, _meta: {
|
|
50
|
+
'com.hypequery/clientContractHash': authorizedHash,
|
|
51
|
+
'com.hypequery/manifestHash': schemas.manifestHash,
|
|
52
|
+
} };
|
|
53
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ProtocolClientContract } from '@hypequery/protocol';
|
|
2
|
+
import type { MCPQueryLimits } from '../../types.js';
|
|
3
|
+
/** Lower operational limits only; the backend's endpoint schema hashes stay identical. */
|
|
4
|
+
export declare function mcpClientProfile(contract: ProtocolClientContract, configured?: MCPQueryLimits): ProtocolClientContract;
|
|
5
|
+
//# sourceMappingURL=profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/utils/http-client/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACjG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,sBAAsB,EAAE,UAAU,CAAC,EAAE,cAAc,GAAG,sBAAsB,CAuBtH"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { prepareProtocolClientContract } from '@hypequery/protocol';
|
|
2
|
+
import { resolveQueryLimits } from '../../tools/utils/query-limits.js';
|
|
3
|
+
import { MCPToolError } from '../../errors.js';
|
|
4
|
+
/** Lower operational limits only; the backend's endpoint schema hashes stay identical. */
|
|
5
|
+
export function mcpClientProfile(contract, configured) {
|
|
6
|
+
const limits = resolveQueryLimits(undefined, configured);
|
|
7
|
+
try {
|
|
8
|
+
return prepareProtocolClientContract({
|
|
9
|
+
kind: contract.kind, version: contract.version, view: contract.view,
|
|
10
|
+
endpoints: contract.endpoints.map(({ schemaHash: _hash, ...endpoint }) => ({
|
|
11
|
+
...endpoint,
|
|
12
|
+
limits: {
|
|
13
|
+
...endpoint.limits,
|
|
14
|
+
maxLimit: Math.min(endpoint.limits.maxLimit, limits.maxResultSize, 1000),
|
|
15
|
+
defaultLimit: Math.min(endpoint.limits.defaultLimit, limits.defaultResultSize, limits.maxResultSize, 1000),
|
|
16
|
+
maxOffset: Math.min(endpoint.limits.maxOffset, limits.maxOffset),
|
|
17
|
+
maxDimensions: Math.min(endpoint.limits.maxDimensions, limits.maxDimensions),
|
|
18
|
+
maxMeasures: Math.min(endpoint.limits.maxMeasures, limits.maxMeasures),
|
|
19
|
+
maxPredicates: Math.min(endpoint.limits.maxPredicates, limits.maxFilters),
|
|
20
|
+
maxOrderBy: Math.min(endpoint.limits.maxOrderBy, limits.maxOrderBy),
|
|
21
|
+
maxFilterValues: Math.min(endpoint.limits.maxFilterValues, 1000),
|
|
22
|
+
},
|
|
23
|
+
})),
|
|
24
|
+
}).contract;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new MCPToolError('MCP_CATALOG_TOO_LARGE', 'Client defaults exceed MCP query ceilings; publish a narrower profile.', { retryable: false });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ClientQueryPlan } from '@hypequery/protocol';
|
|
2
|
+
/** Duplicate-aware bounded JSON, closed public metadata and exact requested row fields. */
|
|
3
|
+
export declare function clientHttpResult(text: string, plan: ClientQueryPlan): Record<string, unknown>;
|
|
4
|
+
//# sourceMappingURL=result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../../src/utils/http-client/result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AA2B3D,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA2C7F"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { MCPExecutionBudgetError, MCPToolError } from '../../errors.js';
|
|
2
|
+
import { decodeHttpResultJson, HttpResultJsonBudgetError } from './json.js';
|
|
3
|
+
function invalid() {
|
|
4
|
+
throw new MCPToolError('MCP_EXECUTION_FAILED', 'Backend returned an invalid client result.', { retryable: false });
|
|
5
|
+
}
|
|
6
|
+
function record(value, allowed) {
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
|
+
return invalid();
|
|
9
|
+
if (allowed && Object.keys(value).some(key => !allowed.includes(key)))
|
|
10
|
+
return invalid();
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function integer(value, minimum = 0) {
|
|
14
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
|
|
15
|
+
}
|
|
16
|
+
function string(value) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
return false;
|
|
19
|
+
for (let index = 0; index < value.length; index++) {
|
|
20
|
+
const code = value.charCodeAt(index);
|
|
21
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
22
|
+
const low = value.charCodeAt(++index);
|
|
23
|
+
if (!(low >= 0xdc00 && low <= 0xdfff))
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/** Duplicate-aware bounded JSON, closed public metadata and exact requested row fields. */
|
|
32
|
+
export function clientHttpResult(text, plan) {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = decodeHttpResultJson(text);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof HttpResultJsonBudgetError)
|
|
39
|
+
throw new MCPExecutionBudgetError('MCP_RESULT_TOO_LARGE', 'Backend result exceeds JSON limits.');
|
|
40
|
+
return invalid();
|
|
41
|
+
}
|
|
42
|
+
const result = record(parsed, ['data', 'meta']);
|
|
43
|
+
if (!Array.isArray(result.data) || result.data.length > plan.effectiveLimit)
|
|
44
|
+
return invalid();
|
|
45
|
+
const dimensions = plan.body.dimensions;
|
|
46
|
+
const fields = [...dimensions, ...plan.selectedMeasures, ...(plan.body.by === undefined ? [] : ['period'])];
|
|
47
|
+
for (const item of result.data) {
|
|
48
|
+
const row = record(item, fields);
|
|
49
|
+
if (Object.keys(row).length !== fields.length || fields.some(field => !Object.hasOwn(row, field)))
|
|
50
|
+
return invalid();
|
|
51
|
+
for (const field of fields) {
|
|
52
|
+
const value = row[field];
|
|
53
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
54
|
+
return invalid();
|
|
55
|
+
if (field === 'period' && plan.body.by !== undefined && value !== null
|
|
56
|
+
&& (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2})?$/.test(value)))
|
|
57
|
+
return invalid();
|
|
58
|
+
const dimension = Object.hasOwn(plan.endpoint.dimensions, field) && dimensions.includes(field) ? plan.endpoint.dimensions[field] : undefined;
|
|
59
|
+
if (value === null) {
|
|
60
|
+
if (dimension?.nullable === false)
|
|
61
|
+
return invalid();
|
|
62
|
+
}
|
|
63
|
+
else if (!dimension) {
|
|
64
|
+
if (!string(value))
|
|
65
|
+
return invalid();
|
|
66
|
+
}
|
|
67
|
+
else if (dimension.wireType === 'number-or-string') {
|
|
68
|
+
if (!(string(value) || typeof value === 'number' && Number.isFinite(value)))
|
|
69
|
+
return invalid();
|
|
70
|
+
}
|
|
71
|
+
else if (dimension.wireType === 'string') {
|
|
72
|
+
if (!string(value))
|
|
73
|
+
return invalid();
|
|
74
|
+
}
|
|
75
|
+
else if (typeof value !== dimension.wireType)
|
|
76
|
+
return invalid();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (result.meta !== undefined && result.meta !== null) {
|
|
80
|
+
if (!plan.endpoint.metadata)
|
|
81
|
+
return invalid();
|
|
82
|
+
const meta = record(result.meta, ['requestId', 'timingMs', 'rowCount', 'pagination', 'cache']);
|
|
83
|
+
if (!string(meta.requestId) || meta.requestId.length === 0 || meta.requestId.length > 1024
|
|
84
|
+
|| typeof meta.timingMs !== 'number' || meta.timingMs < 0 || !Number.isFinite(meta.timingMs)
|
|
85
|
+
|| meta.rowCount !== result.data.length)
|
|
86
|
+
return invalid();
|
|
87
|
+
const pagination = record(meta.pagination, ['limit', 'offset', 'hasMore']);
|
|
88
|
+
if (!integer(pagination.limit, 1) || pagination.limit > plan.effectiveLimit
|
|
89
|
+
|| pagination.offset !== plan.body.offset || typeof pagination.hasMore !== 'boolean'
|
|
90
|
+
|| result.data.length > pagination.limit || pagination.hasMore && result.data.length !== pagination.limit)
|
|
91
|
+
return invalid();
|
|
92
|
+
if (meta.cache !== undefined && typeof record(meta.cache, ['hit']).hit !== 'boolean')
|
|
93
|
+
return invalid();
|
|
94
|
+
}
|
|
95
|
+
else if (plan.body.includeMeta === true)
|
|
96
|
+
return invalid();
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type BackendHeaders = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
2
|
+
export interface HttpBackend {
|
|
3
|
+
readonly discovery: URL;
|
|
4
|
+
readonly headers?: BackendHeaders;
|
|
5
|
+
readonly fetchFn: typeof fetch;
|
|
6
|
+
}
|
|
7
|
+
export declare function backendUrl(input: string): URL;
|
|
8
|
+
export declare function backendRequest(backend: HttpBackend, url: URL, signal: AbortSignal, body?: Readonly<Record<string, unknown>>, schemaHash?: string): Promise<Response>;
|
|
9
|
+
export declare function discardBackendBody(response: Response): void;
|
|
10
|
+
/** Bound decompressed streamed bytes, including when Content-Length is absent or dishonest. */
|
|
11
|
+
export declare function backendText(response: Response, maximum: number, signal: AbortSignal): Promise<string>;
|
|
12
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/utils/http-client/transport.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AACvH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC;CAChC;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAQ7C;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GACjH,OAAO,CAAC,QAAQ,CAAC,CAkBnB;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAE3D;AAED,+FAA+F;AAC/F,wBAAsB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAsC3G"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { MCPExecutionBudgetError, MCPToolError } from '../../errors.js';
|
|
2
|
+
export function backendUrl(input) {
|
|
3
|
+
try {
|
|
4
|
+
const url = new URL(input);
|
|
5
|
+
const local = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
6
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && local)
|
|
7
|
+
|| url.username || url.password || url.hash || url.search)
|
|
8
|
+
throw new Error();
|
|
9
|
+
return url;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error('Use an HTTPS discovery URL (HTTP is allowed only on loopback), without credentials, query or fragment.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function backendRequest(backend, url, signal, body, schemaHash) {
|
|
16
|
+
if (url.origin !== backend.discovery.origin)
|
|
17
|
+
throw new MCPToolError('MCP_EXECUTION_FAILED', 'Backend origin changed.', { retryable: false });
|
|
18
|
+
try {
|
|
19
|
+
signal.throwIfAborted();
|
|
20
|
+
const configured = typeof backend.headers === 'function' ? await backend.headers() : backend.headers;
|
|
21
|
+
const headers = new Headers(configured);
|
|
22
|
+
headers.set('accept', 'application/json');
|
|
23
|
+
if (body)
|
|
24
|
+
headers.set('content-type', 'application/json');
|
|
25
|
+
if (schemaHash)
|
|
26
|
+
headers.set('x-hypequery-client-schema', schemaHash);
|
|
27
|
+
signal.throwIfAborted();
|
|
28
|
+
return await backend.fetchFn(url.href, {
|
|
29
|
+
method: body ? 'POST' : 'GET', headers, body: body ? JSON.stringify(body) : undefined,
|
|
30
|
+
redirect: 'manual', credentials: 'omit', cache: 'no-store', signal,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
if (signal.aborted)
|
|
35
|
+
throw signal.reason;
|
|
36
|
+
throw new MCPToolError('MCP_EXECUTION_FAILED', 'Backend request failed.', { retryable: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function discardBackendBody(response) {
|
|
40
|
+
void response.body?.cancel().catch(() => undefined);
|
|
41
|
+
}
|
|
42
|
+
/** Bound decompressed streamed bytes, including when Content-Length is absent or dishonest. */
|
|
43
|
+
export async function backendText(response, maximum, signal) {
|
|
44
|
+
const length = response.headers.get('content-length');
|
|
45
|
+
const type = response.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase();
|
|
46
|
+
if (type !== 'application/json' && !/^application\/[a-z0-9.+-]+\+json$/.test(type ?? '')) {
|
|
47
|
+
discardBackendBody(response);
|
|
48
|
+
throw new MCPToolError('MCP_EXECUTION_FAILED', 'Backend did not return JSON.', { retryable: false });
|
|
49
|
+
}
|
|
50
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > maximum)) {
|
|
51
|
+
discardBackendBody(response);
|
|
52
|
+
throw new MCPExecutionBudgetError('MCP_RESULT_TOO_LARGE', 'Backend response exceeds its byte limit.');
|
|
53
|
+
}
|
|
54
|
+
const reader = response.body?.getReader();
|
|
55
|
+
if (!reader)
|
|
56
|
+
throw new MCPToolError('MCP_EXECUTION_FAILED', 'Backend returned no response body.', { retryable: false });
|
|
57
|
+
const cancel = () => { void reader.cancel().catch(() => undefined); };
|
|
58
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
59
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
60
|
+
let size = 0;
|
|
61
|
+
let text = '';
|
|
62
|
+
try {
|
|
63
|
+
signal.throwIfAborted();
|
|
64
|
+
while (true) {
|
|
65
|
+
const chunk = await reader.read();
|
|
66
|
+
signal.throwIfAborted();
|
|
67
|
+
if (chunk.done)
|
|
68
|
+
break;
|
|
69
|
+
size += chunk.value.byteLength;
|
|
70
|
+
if (size > maximum)
|
|
71
|
+
throw new MCPExecutionBudgetError('MCP_RESULT_TOO_LARGE', 'Backend response exceeds its byte limit.');
|
|
72
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
73
|
+
}
|
|
74
|
+
return text + decoder.decode();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
cancel();
|
|
78
|
+
if (signal.aborted)
|
|
79
|
+
throw signal.reason;
|
|
80
|
+
if (error instanceof MCPExecutionBudgetError)
|
|
81
|
+
throw error;
|
|
82
|
+
throw new MCPToolError('MCP_EXECUTION_FAILED', 'Backend response could not be read.', { retryable: false });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
signal.removeEventListener('abort', cancel);
|
|
86
|
+
reader.releaseLock();
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const MCP_PACKAGE_VERSION = "0.
|
|
1
|
+
export declare const MCP_PACKAGE_VERSION = "0.10.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,mBAAmB,WAAW,CAAC"}
|
package/dist/version.js
CHANGED