@pdpp/mcp-server 0.4.0 → 0.6.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/package.json +1 -1
- package/src/tools.js +57 -3
package/package.json
CHANGED
package/src/tools.js
CHANGED
|
@@ -156,6 +156,49 @@ class MalformedExpandLimitError extends Error {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// Thrown when `expand` is requested on a stream whose schema advertises no
|
|
160
|
+
// expand_capabilities. The RS would also reject this with invalid_expand, but
|
|
161
|
+
// the MCP adapter catches it first so the error message names the stream and
|
|
162
|
+
// the canonical fix (consult schema before constructing expand arguments).
|
|
163
|
+
class UnadvertisedExpandError extends Error {
|
|
164
|
+
constructor(stream, relations) {
|
|
165
|
+
const relClause = relations.length
|
|
166
|
+
? `Relations requested: ${relations.join(', ')}. `
|
|
167
|
+
: '';
|
|
168
|
+
super(
|
|
169
|
+
`Stream '${stream}' has no advertised expand_capabilities. ${relClause}` +
|
|
170
|
+
"Consult GET /v1/schema (expand_capabilities) before passing expand; " +
|
|
171
|
+
"unadvertised relations are rejected.",
|
|
172
|
+
);
|
|
173
|
+
this.name = 'UnadvertisedExpandError';
|
|
174
|
+
this.code = 'invalid_expand';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Fetch the compact schema for `stream` and throw UnadvertisedExpandError when
|
|
179
|
+
// no expand_capabilities are advertised. Called only when the caller has
|
|
180
|
+
// already supplied an `expand` argument, so the extra RS round-trip is bounded
|
|
181
|
+
// to expand-using calls. The connection_id is forwarded so multi-source grants
|
|
182
|
+
// resolve cleanly.
|
|
183
|
+
async function assertExpandCapabilities(rs, stream, relations, connectionId) {
|
|
184
|
+
const schemaQuery = { view: 'compact', stream };
|
|
185
|
+
if (connectionId) schemaQuery.connection_id = connectionId;
|
|
186
|
+
const schemaResp = await rs.getJson('/v1/schema', { query: schemaQuery });
|
|
187
|
+
if (!schemaResp.ok) return; // schema unavailable — let RS enforce
|
|
188
|
+
const schemaDoc = unwrapSchemaBody(schemaResp.body);
|
|
189
|
+
const streams = schemaDoc?.streams ?? [];
|
|
190
|
+
// Find any stream row that matches (may be multiple for shared stream names).
|
|
191
|
+
const matchingStream = Array.isArray(streams)
|
|
192
|
+
? streams.find((s) => s && (s.name === stream || s.stream === stream || s.stream_name === stream))
|
|
193
|
+
: null;
|
|
194
|
+
if (!matchingStream) return; // unknown stream — let RS enforce
|
|
195
|
+
const expandCaps = matchingStream.expand_capabilities;
|
|
196
|
+
const hasExpandCaps = Array.isArray(expandCaps) ? expandCaps.length > 0 : Boolean(expandCaps);
|
|
197
|
+
if (!hasExpandCaps) {
|
|
198
|
+
throw new UnadvertisedExpandError(stream, relations);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
159
202
|
// Thrown when a self-contained fetch id embeds one connection while the
|
|
160
203
|
// explicit `connection_id` argument names another. Silently preferring either
|
|
161
204
|
// handle could read the wrong source, so the disagreement is rejected with a
|
|
@@ -433,6 +476,9 @@ export function buildTools({ rs, providerUrl }) {
|
|
|
433
476
|
outputSchema: z.object(READ_OUTPUT_SCHEMA_SHAPE),
|
|
434
477
|
handler: async (args) => {
|
|
435
478
|
const stream = requireSafeName(args?.stream, 'stream');
|
|
479
|
+
if (Array.isArray(args?.expand) && args.expand.length > 0) {
|
|
480
|
+
await assertExpandCapabilities(rs, stream, args.expand, args?.connection_id);
|
|
481
|
+
}
|
|
436
482
|
const query = applyExpandLimitToQuery(
|
|
437
483
|
applyFilterToQuery(pickQuery(args, SUPPORTED_QUERY_KEYS), args?.filter),
|
|
438
484
|
args?.expand_limit,
|
|
@@ -449,7 +495,7 @@ export function buildTools({ rs, providerUrl }) {
|
|
|
449
495
|
name: 'aggregate',
|
|
450
496
|
title: 'Aggregate PDPP records',
|
|
451
497
|
description:
|
|
452
|
-
'Compute a single-stream aggregation via `GET /v1/streams/{stream}/aggregate`. Prefer over `query_records` when you only need a count, sum, min/max, distinct count, or grouped/time-bucketed rollup — returns small bucket rows, never record bodies. Metrics: `count`, `sum`, `min`, `max`, `count_distinct` (`field` required for all but `count`). Group with one dimension: `group_by` XOR `group_by_time` (requires `granularity`). Groupable fields are advertised by `GET /v1/schema`. Forwards args verbatim. ' +
|
|
498
|
+
'Compute a single-stream aggregation via `GET /v1/streams/{stream}/aggregate`. Prefer over `query_records` when you only need a count, sum, min/max, distinct count, or grouped/time-bucketed rollup — returns small bucket rows, never record bodies. Metrics: `count`, `sum`, `min`, `max`, `count_distinct` (`field` required for all but `count`). Group with one dimension: `group_by` XOR `group_by_time` (requires `granularity`). Grouped responses include `other_count` (records in groups beyond `limit`) so you can detect top-N truncation without a second call. Groupable fields are advertised by `GET /v1/schema`. Forwards args verbatim. ' +
|
|
453
499
|
CANONICAL_SCHEMA_HINT +
|
|
454
500
|
' Read-only.',
|
|
455
501
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
@@ -565,6 +611,10 @@ export function buildTools({ rs, providerUrl }) {
|
|
|
565
611
|
if (ref.connectionId && typeof args?.connection_id === 'string' && args.connection_id !== ref.connectionId) {
|
|
566
612
|
throw new ConflictingConnectionIdError(ref.connectionId, args.connection_id);
|
|
567
613
|
}
|
|
614
|
+
if (Array.isArray(args?.expand) && args.expand.length > 0) {
|
|
615
|
+
const connId = args?.connection_id ?? ref.connectionId;
|
|
616
|
+
await assertExpandCapabilities(rs, ref.stream, args.expand, connId);
|
|
617
|
+
}
|
|
568
618
|
const query = applyExpandLimitToQuery(pickQuery(args, SUPPORTED_QUERY_KEYS), args?.expand_limit);
|
|
569
619
|
// A self-contained id carries its own connection scope; forward it so a
|
|
570
620
|
// multi-source grant resolves without a second model-carried handle.
|
|
@@ -1035,7 +1085,8 @@ function summarizeAggregate(body, stream) {
|
|
|
1035
1085
|
return `${formatScalar(key)}=${count == null ? '?' : count}`;
|
|
1036
1086
|
});
|
|
1037
1087
|
const more = groups.length > AGGREGATE_GROUP_PREVIEW_LIMIT ? ` more_groups=${groups.length - AGGREGATE_GROUP_PREVIEW_LIMIT};` : '';
|
|
1038
|
-
|
|
1088
|
+
const otherCount = typeof agg.other_count === 'number' ? ` other_count=${agg.other_count};` : '';
|
|
1089
|
+
return `${head} ${dimension}: ${groups.length} group(s) [${shown.join(', ')}]${more}${otherCount} canonical envelope in structuredContent.data`;
|
|
1039
1090
|
}
|
|
1040
1091
|
|
|
1041
1092
|
// Ungrouped: the scalar answer lives in `value`. Fall back to
|
|
@@ -1610,13 +1661,14 @@ function summarizeSearch(body, results) {
|
|
|
1610
1661
|
const hasMore = envelopeField(body, 'has_more') === true ? ' has_more=true.' : '';
|
|
1611
1662
|
const nextCursor = envelopeStringField(body, 'next_cursor');
|
|
1612
1663
|
const cursorText = nextCursor ? ` next_cursor=${formatScalar(nextCursor)}.` : '';
|
|
1664
|
+
const firstFetchText = results[0]?.id ? ` first_fetch_id=${formatInlineValue(results[0].id)}` : '';
|
|
1613
1665
|
const sourceMixText = formatSearchSourceMix(body);
|
|
1614
1666
|
const previews = results.slice(0, SEARCH_TEXT_PREVIEW_LIMIT).map(formatSearchPreviewLine);
|
|
1615
1667
|
const previewText = previews.length > 0 ? ` Top results:\n${previews.join('\n')}` : '';
|
|
1616
1668
|
const fetchHint = previews.length > 0
|
|
1617
1669
|
? '\nFetch a hit with `fetch` using the shown id as-is; ids are self-contained. Pass connection_id only when shown separately.'
|
|
1618
1670
|
: '';
|
|
1619
|
-
return `search: ${results.length} hit(s).${hasMore}${cursorText}${sourceMixText}${previewText}${fetchHint} Search envelope metadata: structuredContent.data; flattened results: structuredContent.results.`;
|
|
1671
|
+
return `search: ${results.length} hit(s).${hasMore}${cursorText}${firstFetchText}${sourceMixText}${previewText}${fetchHint} Search envelope metadata: structuredContent.data; flattened results: structuredContent.results.`;
|
|
1620
1672
|
}
|
|
1621
1673
|
|
|
1622
1674
|
function formatSearchSourceMix(body) {
|
|
@@ -2147,4 +2199,6 @@ export const __internal = {
|
|
|
2147
2199
|
toFetchToolResult,
|
|
2148
2200
|
resolveStreamName,
|
|
2149
2201
|
resolveSchemaDetail,
|
|
2202
|
+
assertExpandCapabilities,
|
|
2203
|
+
UnadvertisedExpandError,
|
|
2150
2204
|
};
|