@pdpp/mcp-server 0.4.0 → 0.5.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.js +52 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdpp/mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Local stdio MCP adapter for grant-scoped PDPP reads and event-subscription management.",
5
5
  "type": "module",
6
6
  "bin": {
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,
@@ -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.
@@ -2147,4 +2197,6 @@ export const __internal = {
2147
2197
  toFetchToolResult,
2148
2198
  resolveStreamName,
2149
2199
  resolveSchemaDetail,
2200
+ assertExpandCapabilities,
2201
+ UnadvertisedExpandError,
2150
2202
  };