@pdpp/mcp-server 0.15.0 → 0.15.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/package.json +2 -1
- package/src/tools.js +95 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdpp/mcp-server",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Local stdio MCP adapter for grant-scoped PDPP reads and event-subscription management.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "node --test test/*.test.js",
|
|
20
|
+
"test:read-surface": "node --test test/hostile-client-read-evidence.test.js test/server.integration.test.js",
|
|
20
21
|
"verify": "pnpm test && node bin/pdpp-mcp-server.js --help",
|
|
21
22
|
"pack:dry-run": "pnpm pack --dry-run"
|
|
22
23
|
},
|
package/src/tools.js
CHANGED
|
@@ -645,10 +645,10 @@ export function buildTools({ rs, providerUrl }) {
|
|
|
645
645
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
646
646
|
inputSchema: z
|
|
647
647
|
.object({
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
648
|
+
id: z
|
|
649
|
+
.string()
|
|
650
|
+
.min(1)
|
|
651
|
+
.describe('ID: `connection_id/stream:record_id`, `stream:record_id`, or `pdpp://record/...` URI.'),
|
|
652
652
|
expand: z.array(z.string()).optional().describe(EXPAND_DESCRIPTION),
|
|
653
653
|
expand_limit: z
|
|
654
654
|
.record(z.string(), z.number().int().positive())
|
|
@@ -684,11 +684,15 @@ export function buildTools({ rs, providerUrl }) {
|
|
|
684
684
|
{
|
|
685
685
|
name: 'read_record_field',
|
|
686
686
|
title: 'Read PDPP record field window',
|
|
687
|
-
description: 'Read bounded field text
|
|
687
|
+
description: 'Read bounded field text by record id or `pdpp://record/...` URI; page by offset, cursor, or q.',
|
|
688
688
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
689
689
|
inputSchema: z
|
|
690
690
|
.object({
|
|
691
|
-
|
|
691
|
+
id: z
|
|
692
|
+
.string()
|
|
693
|
+
.min(1)
|
|
694
|
+
.optional()
|
|
695
|
+
.describe('ID: `connection_id/stream:record_id`, `stream:record_id`, or `pdpp://record/...` URI.'),
|
|
692
696
|
connection_id: z.string().min(1).optional(),
|
|
693
697
|
stream: z.string().min(1).optional(),
|
|
694
698
|
record_id: z.string().min(1).optional(),
|
|
@@ -932,7 +936,14 @@ function resourceErrorContents(uri, response, providerUrl) {
|
|
|
932
936
|
}
|
|
933
937
|
|
|
934
938
|
function resolveRecordResourceRef(uri, variables) {
|
|
935
|
-
const
|
|
939
|
+
const handle = resolveResourceHandle(uri, variables, 'record');
|
|
940
|
+
let payload;
|
|
941
|
+
try {
|
|
942
|
+
payload = decodeResourceHandle(handle, 'record');
|
|
943
|
+
} catch (error) {
|
|
944
|
+
const ref = parseRecordResultId(decodeURIComponent(handle));
|
|
945
|
+
return { connectionId: ref.connectionId, stream: ref.stream, recordId: ref.recordId };
|
|
946
|
+
}
|
|
936
947
|
return {
|
|
937
948
|
connectionId: requireSafeName(payload.connection_id, 'connection_id'),
|
|
938
949
|
stream: requireSafeName(payload.stream, 'stream'),
|
|
@@ -1390,6 +1401,7 @@ function toReadRecordFieldToolResult(response, providerUrl, identity) {
|
|
|
1390
1401
|
const resource = {
|
|
1391
1402
|
uri: resourceUri,
|
|
1392
1403
|
mime_type: 'text/plain',
|
|
1404
|
+
handle_semantics: 'live_lookup',
|
|
1393
1405
|
};
|
|
1394
1406
|
const header = [
|
|
1395
1407
|
`record=${record.id}`,
|
|
@@ -1402,18 +1414,44 @@ function toReadRecordFieldToolResult(response, providerUrl, identity) {
|
|
|
1402
1414
|
]
|
|
1403
1415
|
.filter(Boolean)
|
|
1404
1416
|
.join(' ');
|
|
1417
|
+
const continuationArgs = (offsetChars) => {
|
|
1418
|
+
const args = {
|
|
1419
|
+
id: record.id,
|
|
1420
|
+
field_path: fieldInfo.path,
|
|
1421
|
+
offset_chars: offsetChars,
|
|
1422
|
+
};
|
|
1423
|
+
const limitChars = numberValue(window.limit_chars);
|
|
1424
|
+
if (limitChars !== null) args.limit_chars = limitChars;
|
|
1425
|
+
return args;
|
|
1426
|
+
};
|
|
1427
|
+
const nextOffsetChars = numberValue(window.next_offset_chars);
|
|
1428
|
+
const previousOffsetChars = numberValue(window.previous_offset_chars);
|
|
1429
|
+
const continuationLines =
|
|
1430
|
+
window.complete === true
|
|
1431
|
+
? []
|
|
1432
|
+
: [
|
|
1433
|
+
nextOffsetChars !== null ? `next_offset_chars=${nextOffsetChars}` : null,
|
|
1434
|
+
nextOffsetChars !== null
|
|
1435
|
+
? `next read_record_field args=${JSON.stringify(continuationArgs(nextOffsetChars))}`
|
|
1436
|
+
: null,
|
|
1437
|
+
previousOffsetChars !== null ? `previous_offset_chars=${previousOffsetChars}` : null,
|
|
1438
|
+
previousOffsetChars !== null
|
|
1439
|
+
? `previous read_record_field args=${JSON.stringify(continuationArgs(previousOffsetChars))}`
|
|
1440
|
+
: null,
|
|
1441
|
+
].filter(Boolean);
|
|
1405
1442
|
const text = typeof window.text === 'string' ? window.text : '';
|
|
1406
1443
|
return {
|
|
1407
1444
|
content: [
|
|
1408
1445
|
{
|
|
1409
1446
|
type: 'text',
|
|
1410
|
-
text:
|
|
1447
|
+
text: [header, ...continuationLines, text].join('\n'),
|
|
1411
1448
|
},
|
|
1412
1449
|
],
|
|
1413
1450
|
structuredContent: {
|
|
1414
1451
|
record,
|
|
1415
1452
|
field: fieldInfo,
|
|
1416
1453
|
window,
|
|
1454
|
+
resource,
|
|
1417
1455
|
provider_url: providerUrl,
|
|
1418
1456
|
request_id: response.requestId ?? null,
|
|
1419
1457
|
},
|
|
@@ -1474,11 +1512,12 @@ function buildSearchContentLadder(results) {
|
|
|
1474
1512
|
connectionId: result.connection_id,
|
|
1475
1513
|
});
|
|
1476
1514
|
if (!record || !Array.isArray(result.match_windows) || result.match_windows.length === 0) return record;
|
|
1477
|
-
const
|
|
1515
|
+
const recordId = firstString(record.id);
|
|
1516
|
+
const evidenceExcerpts = searchEvidenceExcerpts(result.match_windows, recordId);
|
|
1478
1517
|
return {
|
|
1479
1518
|
...record,
|
|
1480
1519
|
...(evidenceExcerpts.length > 0 ? { evidence_excerpts: evidenceExcerpts } : {}),
|
|
1481
|
-
field_windows: mergeSearchMatchWindows(record.field_windows, result.match_windows,
|
|
1520
|
+
field_windows: mergeSearchMatchWindows(record.field_windows, result.match_windows, recordId),
|
|
1482
1521
|
};
|
|
1483
1522
|
})
|
|
1484
1523
|
.filter(Boolean)
|
|
@@ -1532,8 +1571,18 @@ function searchEvidencePreviewText(window) {
|
|
|
1532
1571
|
|
|
1533
1572
|
function searchMatchWindowRead(window, recordId, fieldPath) {
|
|
1534
1573
|
const read = objectValue(window?.read);
|
|
1535
|
-
|
|
1574
|
+
// RS `evidence_excerpts` carry no `read` hint of their own. Synthesize a
|
|
1575
|
+
// bounded `read_record_field` continuation from the hit id + matched field so
|
|
1576
|
+
// a visible search excerpt is never a dead end for content-only clients.
|
|
1577
|
+
if (!read) {
|
|
1578
|
+
if (!recordId || !fieldPath) return null;
|
|
1579
|
+
return {
|
|
1580
|
+
tool: 'read_record_field',
|
|
1581
|
+
args: searchMatchWindowReadArgs({}, recordId, fieldPath),
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1536
1584
|
const args = searchMatchWindowReadArgs(read.args, recordId, fieldPath);
|
|
1585
|
+
if (!firstString(args.id, args.record_uri)) return null;
|
|
1537
1586
|
return {
|
|
1538
1587
|
tool: read.tool ?? 'read_record_field',
|
|
1539
1588
|
args,
|
|
@@ -2424,8 +2473,14 @@ function normalizeSearchMatchWindows(hit, options = {}) {
|
|
|
2424
2473
|
hit?.match_windows,
|
|
2425
2474
|
hit?.field_windows,
|
|
2426
2475
|
hit?.matches,
|
|
2476
|
+
// The RS lexical search envelope surfaces proven matched text as
|
|
2477
|
+
// `evidence_excerpts` ({ field_path, preview_text, truncated }). Treat it as
|
|
2478
|
+
// a first-class match-window source so visible search content includes the
|
|
2479
|
+
// bounded excerpt without depending on host-side structuredContent reads.
|
|
2480
|
+
hit?.evidence_excerpts,
|
|
2427
2481
|
hit?.data?.match_windows,
|
|
2428
2482
|
hit?.data?.field_windows,
|
|
2483
|
+
hit?.data?.evidence_excerpts,
|
|
2429
2484
|
].find((value) => Array.isArray(value));
|
|
2430
2485
|
if (!Array.isArray(candidates)) return [];
|
|
2431
2486
|
return candidates
|
|
@@ -2444,17 +2499,22 @@ function normalizeSearchMatchWindowReadHints(matchWindows, recordId) {
|
|
|
2444
2499
|
function normalizeSearchMatchWindow(window, options = {}) {
|
|
2445
2500
|
const value = objectValue(window);
|
|
2446
2501
|
if (!value) return null;
|
|
2447
|
-
|
|
2502
|
+
// `preview_text` is the RS `evidence_excerpt` text field; `text`/`preview`/
|
|
2503
|
+
// `snippet` cover match-window and legacy hit shapes.
|
|
2504
|
+
const text = firstString(value.text, value.preview_text, value.preview, value.snippet, value.window?.text);
|
|
2448
2505
|
const fieldPath = firstString(value.field_path, value.fieldPath, value.path, value.field?.path);
|
|
2449
2506
|
if (!text || !fieldPath) return null;
|
|
2450
2507
|
const read = objectValue(value.read);
|
|
2451
2508
|
const normalizedText = centeredSearchText(text, options.q, SEARCH_RESULT_SNIPPET_CHAR_LIMIT);
|
|
2452
2509
|
const previewText = centeredSearchText(text, options.q, SEARCH_TEXT_SNIPPET_CHAR_LIMIT);
|
|
2510
|
+
// An evidence_excerpt that is `truncated: true` is an incomplete window; a
|
|
2511
|
+
// match window declares completeness explicitly via `complete: true`.
|
|
2512
|
+
const complete = value.complete === true || value.truncated === false;
|
|
2453
2513
|
return {
|
|
2454
2514
|
field_path: fieldPath,
|
|
2455
2515
|
text: normalizedText,
|
|
2456
2516
|
preview_text: previewText,
|
|
2457
|
-
complete
|
|
2517
|
+
complete,
|
|
2458
2518
|
...(firstString(value.next_cursor, value.window?.next_cursor) ? { next_cursor: firstString(value.next_cursor, value.window?.next_cursor) } : {}),
|
|
2459
2519
|
...(read ? { read } : {}),
|
|
2460
2520
|
};
|
|
@@ -2540,7 +2600,7 @@ function compactSearchEnvelopeDataObject(data, { resultCount } = {}) {
|
|
|
2540
2600
|
}
|
|
2541
2601
|
|
|
2542
2602
|
function resultIdForHit(hit, index) {
|
|
2543
|
-
const directId = stringValue(hit?.result_id ?? hit?.resultId);
|
|
2603
|
+
const directId = stringValue(hit?.result_id ?? hit?.resultId ?? hit?.record_uri ?? hit?.recordUri);
|
|
2544
2604
|
if (directId) return directId;
|
|
2545
2605
|
|
|
2546
2606
|
const stream = streamForHit(hit);
|
|
@@ -2585,7 +2645,25 @@ function parseRecordResultId(id) {
|
|
|
2585
2645
|
throw new Error('id is required');
|
|
2586
2646
|
}
|
|
2587
2647
|
if (id.startsWith('pdpp://record/')) {
|
|
2588
|
-
|
|
2648
|
+
const handle = decodeURIComponent(id.slice('pdpp://record/'.length));
|
|
2649
|
+
// The canonical record_uri the model sees in content ladders and resource
|
|
2650
|
+
// templates is `pdpp://record/{base64url-JSON}`. Accept that first so a
|
|
2651
|
+
// visible handle is never a dead fetch/read_record_field id; fall back to
|
|
2652
|
+
// the human-readable `connection_id/stream:record_id` form for legacy/test
|
|
2653
|
+
// URIs that embed the self-contained grammar directly.
|
|
2654
|
+
try {
|
|
2655
|
+
const payload = decodeResourceHandle(handle, 'record');
|
|
2656
|
+
return {
|
|
2657
|
+
connectionId: requireSafeName(payload.connection_id, 'connection_id'),
|
|
2658
|
+
stream: requireSafeName(payload.stream, 'stream'),
|
|
2659
|
+
recordId: requireSafeName(payload.record_id, 'record_id'),
|
|
2660
|
+
};
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
if (error instanceof InvalidResourceUriError) {
|
|
2663
|
+
return parseRecordResultId(handle);
|
|
2664
|
+
}
|
|
2665
|
+
throw error;
|
|
2666
|
+
}
|
|
2589
2667
|
}
|
|
2590
2668
|
const connectionSeparator = id.indexOf('/');
|
|
2591
2669
|
if (connectionSeparator === -1) {
|
|
@@ -2921,4 +2999,6 @@ export const __internal = {
|
|
|
2921
2999
|
resolveSchemaDetail,
|
|
2922
3000
|
assertExpandCapabilities,
|
|
2923
3001
|
UnadvertisedExpandError,
|
|
3002
|
+
parseRecordResultId,
|
|
3003
|
+
encodeResourceUri,
|
|
2924
3004
|
};
|