@333eco/corpus 2.4.1 → 2.4.2
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 +19 -11
- package/dist/corpus.json +1 -1
- package/package.json +1 -1
- package/src/read-tools.mjs +23 -32
- package/src/search.mjs +3 -0
- package/src/server.mjs +5 -40
package/README.md
CHANGED
|
@@ -113,8 +113,8 @@ corpus already fits one — but a way to read the shelf.
|
|
|
113
113
|
- **Paged by size.** `max_bytes` defaults to 90,000 (under the 25,000-token point at which
|
|
114
114
|
Claude Code moves a tool result into a file) and may go to 400,000. **A document is never
|
|
115
115
|
split** — half a document verifies nothing — so one larger than the page arrives alone and
|
|
116
|
-
says
|
|
117
|
-
rather than serving page 2 of another corpus.
|
|
116
|
+
says so in its `[PAGE]` line. That last block names the cursor for the next page; a cursor from a
|
|
117
|
+
different corpus version is refused rather than serving page 2 of another corpus.
|
|
118
118
|
- **`list_documents` says how to read what it listed:** `read_with` carries the same filters as
|
|
119
119
|
`read_documents` arguments.
|
|
120
120
|
|
|
@@ -195,15 +195,23 @@ hands every consumer an obligation nobody can discharge, so **the build fails**
|
|
|
195
195
|
rather than serving it — the same reasoning as the licence gate: a property, not a
|
|
196
196
|
rule someone has to remember.
|
|
197
197
|
|
|
198
|
-
**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
198
|
+
**Documents come as text; everything else is structured.** `get_document` and
|
|
199
|
+
`read_documents` return **`content` only** — each document behind its provenance header
|
|
200
|
+
and ending at its `[END OF DOCUMENT]` line. The other tools return `structuredContent`
|
|
201
|
+
(the typed object) plus a readable or compact `content`, and anything a reader needs —
|
|
202
|
+
search's `reading` line included — is in the structured part.
|
|
203
|
+
|
|
204
|
+
⛔⛔ **Why the document tools carry no `structuredContent` (2.4.2).** From 2.0.0 to 2.4.1
|
|
205
|
+
they split by role: text in `content`, the envelope *without the body* in
|
|
206
|
+
`structuredContent`. A Claude client shown a result that carries `structuredContent` hands its
|
|
207
|
+
model **only that part** — so for eleven days **no document text reached any Claude-based
|
|
208
|
+
reader**, while every check here passed, because every check read the server's output
|
|
209
|
+
directly rather than through a client. The MCP spec assumes the two parts carry the same
|
|
210
|
+
information; a server that splits them is at the mercy of whichever part a client picks. The
|
|
211
|
+
document is the payload and its provenance already rides in the header, so the document tools
|
|
212
|
+
now send the text and nothing else — and `check-parity` fails if either ever carries
|
|
213
|
+
`structuredContent` again. `outputSchema` is deliberately not declared: a schema binds the server
|
|
214
|
+
on every future change.
|
|
207
215
|
|
|
208
216
|
**Text is returned verbatim and is never summarised.** Not a stylistic
|
|
209
217
|
preference — a summary cannot be hash-verified, so summarising at the server
|
package/dist/corpus.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@333eco/corpus",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "An MCP server for an open-licensed corpus, served with verifiable provenance — every document carries its sha256, DOI and OpenTimestamps proof so a consuming agent can check its own citation.",
|
|
5
5
|
"license": "CC0-1.0",
|
|
6
6
|
"author": "Thon Ly",
|
package/src/read-tools.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export const READ_TOOLS = [
|
|
|
43
43
|
// facets are described once, on list_documents.
|
|
44
44
|
description:
|
|
45
45
|
"Whole documents, never summarised: those matching the list_documents filters, or the given slugs. " +
|
|
46
|
-
"One content block each
|
|
46
|
+
"One content block each; the last block names the cursor for the next page.",
|
|
47
47
|
inputSchema: {
|
|
48
48
|
type: "object",
|
|
49
49
|
properties: {
|
|
@@ -110,9 +110,9 @@ const decodeCursor = (cursor, version) => {
|
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* read_documents. `ctx` is what differs between the surfaces:
|
|
113
|
-
* { documents, bySlug,
|
|
113
|
+
* { documents, bySlug, version }.
|
|
114
114
|
*/
|
|
115
|
-
export const readDocuments = ({ documents, bySlug,
|
|
115
|
+
export const readDocuments = ({ documents, bySlug, version }, args) => {
|
|
116
116
|
const hasFilter = ["genre", "category", "voice", "licence"].some((k) => args?.[k]);
|
|
117
117
|
let matched;
|
|
118
118
|
if (args?.slugs !== undefined) {
|
|
@@ -149,43 +149,34 @@ export const readDocuments = ({ documents, bySlug, envelope, version }, args) =>
|
|
|
149
149
|
const next = nextOffset < matched.length ? encodeCursor(version, nextOffset) : undefined;
|
|
150
150
|
|
|
151
151
|
if (!matched.length) {
|
|
152
|
-
return {
|
|
153
|
-
content: [{ type: "text", text: "no documents match these filters. Call list_documents to see the facets and their counts." }],
|
|
154
|
-
structuredContent: { matched: 0, returned: 0, offset: 0, page_bytes: 0, max_bytes: budget, documents: [] }
|
|
155
|
-
};
|
|
152
|
+
return { content: [{ type: "text", text: "no documents match these filters. Call list_documents to see the facets and their counts." }] };
|
|
156
153
|
}
|
|
157
154
|
|
|
155
|
+
// ⛔⛔ TEXT ONLY — NO structuredContent (2.4.2, founder-ruled 2026-09-13). A Claude client shown a
|
|
156
|
+
// result that carries structuredContent hands its model ONLY that part; the envelope-without-body
|
|
157
|
+
// shape this tool shipped with in 2.4.0 therefore delivered no document to any Claude-based reader
|
|
158
|
+
// (§the-text-never-arrived). The document IS the payload, and the provenance already rides in each
|
|
159
|
+
// document's header, so the structured copy bought validation for programs at the price of the
|
|
160
|
+
// text for models. Everything a reader needs to keep going is in the [PAGE] line.
|
|
158
161
|
const content = page.map((p) => ({ type: "text", text: p.text }));
|
|
159
|
-
|
|
160
|
-
// order to keep going, or to know it has everything.
|
|
162
|
+
const over = page.length === 1 && used > budget ? " This document is larger than max_bytes and was returned whole rather than split." : "";
|
|
161
163
|
content.push({
|
|
162
164
|
type: "text",
|
|
163
165
|
text: next
|
|
164
|
-
? `[PAGE — documents ${start + 1}–${nextOffset} of ${matched.length}
|
|
165
|
-
: `[PAGE — documents ${start + 1}–${nextOffset} of ${matched.length}
|
|
166
|
+
? `[PAGE — documents ${start + 1}–${nextOffset} of ${matched.length}.${over} ${matched.length - nextOffset} more: call read_documents again with the same arguments and cursor "${next}".]`
|
|
167
|
+
: `[PAGE — documents ${start + 1}–${nextOffset} of ${matched.length}.${over} That is every matching document.]`
|
|
166
168
|
});
|
|
169
|
+
return { content };
|
|
170
|
+
};
|
|
167
171
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
...(page.length === 1 && used > budget ? { over_budget: "one document larger than max_bytes, returned whole rather than split" } : {}),
|
|
177
|
-
...(next ? { next_cursor: next } : {}),
|
|
178
|
-
documents: page.map((p) => ({
|
|
179
|
-
...envelope(p.d),
|
|
180
|
-
genre: p.d.genre,
|
|
181
|
-
category: p.d.category,
|
|
182
|
-
voice: p.d.voice,
|
|
183
|
-
bytes: p.d.bytes,
|
|
184
|
-
words: p.d.words,
|
|
185
|
-
text_in: "content, one block per document in this order, each prefixed by its provenance header"
|
|
186
|
-
}))
|
|
187
|
-
}
|
|
188
|
-
};
|
|
172
|
+
/**
|
|
173
|
+
* get_document — shared by both surfaces since 2.4.2, TEXT ONLY for the same reason as read_documents:
|
|
174
|
+
* the envelope-without-body in structuredContent was the only part a Claude client showed its model.
|
|
175
|
+
*/
|
|
176
|
+
export const getDocument = ({ bySlug }, args) => {
|
|
177
|
+
const d = bySlug.get(String(args?.slug ?? ""));
|
|
178
|
+
if (!d) throw new Error(`no document with slug "${args?.slug}". Call list_documents to see what is available.`);
|
|
179
|
+
return { content: [{ type: "text", text: documentText(d) }] };
|
|
189
180
|
};
|
|
190
181
|
|
|
191
182
|
/**
|
package/src/search.mjs
CHANGED
|
@@ -259,6 +259,9 @@ export const searchCorpus = ({ documents, envelope }, args) => {
|
|
|
259
259
|
query: q,
|
|
260
260
|
matches: hits.length,
|
|
261
261
|
returned: results.length,
|
|
262
|
+
// ⛔ 2.4.2: the reading line was in `content` only, and a Claude client shows its model only
|
|
263
|
+
// structuredContent — so the 2.4.1 nudge reached no Claude reader. It rides here too.
|
|
264
|
+
...(results.length ? { reading: readingLine(results, hits.length) } : {}),
|
|
262
265
|
...(absent.length ? { absent_terms: absent } : {}),
|
|
263
266
|
results: results
|
|
264
267
|
});
|
package/src/server.mjs
CHANGED
|
@@ -35,12 +35,11 @@ import { dirname, join, resolve } from "node:path";
|
|
|
35
35
|
import { fileURLToPath } from "node:url";
|
|
36
36
|
import { createInterface } from "node:readline";
|
|
37
37
|
import { RESOURCE_TEMPLATES, listResources, readResource, completeArgument } from "./resources.mjs";
|
|
38
|
-
import { structured
|
|
39
|
-
import { documentText } from "./resources.mjs";
|
|
38
|
+
import { structured } from "./results.mjs";
|
|
40
39
|
import { envelope } from "./envelope.mjs";
|
|
41
40
|
import { PROMPTS, getPrompt } from "./prompts.mjs";
|
|
42
41
|
import { BASE_TOOLS, withFacets } from "./base-tools.mjs";
|
|
43
|
-
import { READ_TOOLS, READ_TOOL_NAMES, READ_INSTRUCTIONS, filterDocuments, readDocuments, readWith, completenessOf, manifestProblems } from "./read-tools.mjs";
|
|
42
|
+
import { READ_TOOLS, READ_TOOL_NAMES, READ_INSTRUCTIONS, filterDocuments, readDocuments, getDocument, readWith, completenessOf, manifestProblems } from "./read-tools.mjs";
|
|
44
43
|
import { PROGRAM_TOOLS, PROGRAM_TOOL_NAMES, PROGRAM_INSTRUCTIONS, callProgramTool } from "./program-tools.mjs";
|
|
45
44
|
import { searchCorpus } from "./search.mjs";
|
|
46
45
|
|
|
@@ -127,45 +126,11 @@ const callTool = (name, args) => {
|
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
if (name === "get_document") {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// ⭐ The document goes to `content` WITH ITS PROVENANCE HEADER — the same
|
|
133
|
-
// bytes resources/read returns, so the two doors into a document agree —
|
|
134
|
-
// and the envelope goes to `structuredContent` WITHOUT the body. Split by
|
|
135
|
-
// role; nothing is sent twice.
|
|
136
|
-
return structuredWithText(documentText(d), {
|
|
137
|
-
...envelope(d),
|
|
138
|
-
genre: d.genre,
|
|
139
|
-
bytes: d.bytes,
|
|
140
|
-
words: d.words,
|
|
141
|
-
// ⚠️ category and voice were in list_documents and NOT here, so a
|
|
142
|
-
// caller who fetched ONE document could not see the facets it had
|
|
143
|
-
// just filtered on — the discovery call and the read call disagreed
|
|
144
|
-
// about what a document is. Both are cheap strings; the reason the
|
|
145
|
-
// body is excluded (it is in `content`, never sent twice) does not
|
|
146
|
-
// apply to them.
|
|
147
|
-
category: d.category,
|
|
148
|
-
voice: d.voice,
|
|
149
|
-
repo: d.repo,
|
|
150
|
-
path: d.path,
|
|
151
|
-
metadata_convention: d.metadata_convention,
|
|
152
|
-
// ⚠️ PRESENT ONLY WHERE THEY MEAN SOMETHING, and dropping them was a
|
|
153
|
-
// real bug: `editorial` is what tells a consumer that the inline
|
|
154
|
-
// [VERBATIM]/[SCAFFOLD] markers in `text` are a convention rather
|
|
155
|
-
// than noise, and `segments` is the same split structurally. The
|
|
156
|
-
// markers alone are the load-bearing half — they survive quotation —
|
|
157
|
-
// but shipping them with nothing that explains them made the
|
|
158
|
-
// convention look like an artefact of bad extraction.
|
|
159
|
-
...(d.editorial ? { editorial: d.editorial } : {}),
|
|
160
|
-
...(d.segments ? { segments: d.segments } : {}),
|
|
161
|
-
// ⚠️ Deliberately absent: the body is in `content`. Carrying it here
|
|
162
|
-
// too is the duplication this shape exists to avoid.
|
|
163
|
-
text_in: "content[0].text, prefixed by the provenance header and ending at its [END OF DOCUMENT] line"
|
|
164
|
-
});
|
|
129
|
+
// Shared (read-tools.mjs), and TEXT ONLY since 2.4.2 — see §the-text-never-arrived there.
|
|
130
|
+
return getDocument({ bySlug: bySlug }, args);
|
|
165
131
|
}
|
|
166
|
-
|
|
167
132
|
if (name === "read_documents") {
|
|
168
|
-
return readDocuments({ documents: corpus.documents, bySlug,
|
|
133
|
+
return readDocuments({ documents: corpus.documents, bySlug, version: String(corpus.package_version ?? "") }, args);
|
|
169
134
|
}
|
|
170
135
|
|
|
171
136
|
if (name === "list_documents") {
|