@cueai/omni-reader-mcp 1.1.2 → 1.2.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/dist/server.d.ts CHANGED
@@ -1,3 +1,20 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CAPABILITIES_PROTOCOL_VERSION, DIRECT_GROUNDING_PROFILE, URL_GROUNDING_PROFILE } from "./constants.js";
2
3
  import { type OmniToolDependencies } from "./tools.js";
4
+ export interface ReaderCapabilitiesAdvertisement {
5
+ readonly protocol_version: typeof CAPABILITIES_PROTOCOL_VERSION;
6
+ readonly expires_at: string;
7
+ readonly direct_profiles: readonly [typeof DIRECT_GROUNDING_PROFILE];
8
+ readonly url_profiles: readonly [typeof URL_GROUNDING_PROFILE];
9
+ }
10
+ export declare function buildReaderCapabilitiesAdvertisement(now: Date, ttlSeconds: number): ReaderCapabilitiesAdvertisement;
11
+ /** OMNI_CAPABILITIES_ENABLED gate, default off; a malformed value fails fast. */
12
+ export declare function capabilitiesEnabled(env?: NodeJS.ProcessEnv): boolean;
13
+ /** OMNI_CAPABILITIES_TTL_SECONDS, default 300; malformed/non-positive values fall back. */
14
+ export declare function capabilitiesTtlSeconds(env?: NodeJS.ProcessEnv): number;
15
+ /**
16
+ * The advertisement to include in an initialize result, or null when the flag is off
17
+ * (the custom field must be absent — the server never emits a partial value).
18
+ */
19
+ export declare function readerCapabilitiesAdvertisement(now?: Date): ReaderCapabilitiesAdvertisement | null;
3
20
  export declare function createOmniMcpServer(dependencies: OmniToolDependencies): McpServer;
package/dist/server.js CHANGED
@@ -1,11 +1,52 @@
1
1
  import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { BRIDGE_RELEASE_VERSION } from "./constants.js";
3
+ import { BRIDGE_RELEASE_VERSION, CAPABILITIES_DEFAULT_TTL_SECONDS, CAPABILITIES_ENABLED_ENV, CAPABILITIES_PROTOCOL_VERSION, CAPABILITIES_TTL_SECONDS_ENV, DIRECT_GROUNDING_PROFILE, REMOTE_CAPABILITIES_CUSTOM_FIELD, URL_GROUNDING_PROFILE, } from "./constants.js";
4
4
  import { MACHINE_INSTRUCTIONS } from "./protocol.js";
5
5
  import { registerOmniTools } from "./tools.js";
6
6
  const SERVER_NAME = "omni-reader-mcp";
7
+ /** RFC3339 UTC timestamp at seconds precision (matches the §D2-D example form). */
8
+ function rfc3339Seconds(date) {
9
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
10
+ }
11
+ export function buildReaderCapabilitiesAdvertisement(now, ttlSeconds) {
12
+ return {
13
+ protocol_version: CAPABILITIES_PROTOCOL_VERSION,
14
+ expires_at: rfc3339Seconds(new Date(now.getTime() + ttlSeconds * 1000)),
15
+ direct_profiles: [DIRECT_GROUNDING_PROFILE],
16
+ url_profiles: [URL_GROUNDING_PROFILE],
17
+ };
18
+ }
19
+ /** OMNI_CAPABILITIES_ENABLED gate, default off; a malformed value fails fast. */
20
+ export function capabilitiesEnabled(env = process.env) {
21
+ const raw = (env[CAPABILITIES_ENABLED_ENV] ?? "").trim().toLowerCase();
22
+ if (raw === "true")
23
+ return true;
24
+ if (raw === "" || raw === "false")
25
+ return false;
26
+ throw new Error(`${CAPABILITIES_ENABLED_ENV} must be true or false`);
27
+ }
28
+ /** OMNI_CAPABILITIES_TTL_SECONDS, default 300; malformed/non-positive values fall back. */
29
+ export function capabilitiesTtlSeconds(env = process.env) {
30
+ const raw = env[CAPABILITIES_TTL_SECONDS_ENV];
31
+ if (raw === undefined || raw.trim() === "")
32
+ return CAPABILITIES_DEFAULT_TTL_SECONDS;
33
+ const parsed = Number(raw);
34
+ if (!Number.isInteger(parsed) || parsed <= 0)
35
+ return CAPABILITIES_DEFAULT_TTL_SECONDS;
36
+ return parsed;
37
+ }
38
+ /**
39
+ * The advertisement to include in an initialize result, or null when the flag is off
40
+ * (the custom field must be absent — the server never emits a partial value).
41
+ */
42
+ export function readerCapabilitiesAdvertisement(now = new Date()) {
43
+ if (!capabilitiesEnabled())
44
+ return null;
45
+ return buildReaderCapabilitiesAdvertisement(now, capabilitiesTtlSeconds());
46
+ }
7
47
  export function createOmniMcpServer(dependencies) {
8
48
  const taskStore = new InMemoryTaskStore();
49
+ const advertised = readerCapabilitiesAdvertisement();
9
50
  const server = new McpServer({ name: SERVER_NAME, version: BRIDGE_RELEASE_VERSION }, {
10
51
  instructions: MACHINE_INSTRUCTIONS,
11
52
  taskStore,
@@ -15,6 +56,9 @@ export function createOmniMcpServer(dependencies) {
15
56
  cancel: {},
16
57
  requests: { tools: { call: {} } },
17
58
  },
59
+ ...(advertised === null
60
+ ? {}
61
+ : { experimental: { [REMOTE_CAPABILITIES_CUSTOM_FIELD]: advertised } }),
18
62
  },
19
63
  });
20
64
  registerOmniTools(server, dependencies, taskStore);
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { TaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/interfaces.js";
3
- import type { ArtifactReadResult, LocalResult } from "./artifact-store.js";
3
+ import type { ArtifactReadResult, BundlePartReadChunk, LocalResult } from "./artifact-store.js";
4
+ import type { ReaderCapabilitiesV1 } from "./capabilities.js";
4
5
  import { type CubeGrantClient } from "./cube-client.js";
5
6
  import type { IiisClient, ResultRetentionSink } from "./iiis-client.js";
6
7
  import { type OpenAllowedFileOptions, type OpenedAllowedFile } from "./path-security.js";
@@ -12,6 +13,7 @@ interface ToolRetention extends ResultRetentionSink {
12
13
  interface ToolArtifactStore {
13
14
  createRetention(): ToolRetention;
14
15
  read(resultId: string, cursor?: string, maxBytes?: number): Promise<ArtifactReadResult>;
16
+ readBundlePart?(resultId: string, cursor: string, maxBytes?: number): Promise<BundlePartReadChunk>;
15
17
  discard(resultId: string): Promise<boolean>;
16
18
  }
17
19
  export interface ParseOperationController {
@@ -28,11 +30,13 @@ export interface ParseOperationController {
28
30
  export interface OmniToolDependencies {
29
31
  readonly workspace: string;
30
32
  readonly extraRoots?: readonly string[];
31
- readonly cubeClient: Pick<CubeGrantClient, "createGrant">;
33
+ readonly cubeClient: Pick<CubeGrantClient, "createGrant" | "getCapabilities">;
32
34
  readonly iiisClient: Pick<IiisClient, "uploadAndWait" | "ack">;
33
35
  readonly artifactStore: ToolArtifactStore;
34
36
  readonly createClientRequestId?: () => string;
35
- readonly remoteClient?: RemoteOmniClient;
37
+ readonly remoteClient?: RemoteOmniClient & {
38
+ initializeCapabilities(signal: AbortSignal): Promise<ReaderCapabilitiesV1>;
39
+ };
36
40
  readonly operationManager?: ParseOperationController;
37
41
  readonly openFile?: (localPath: string, options: OpenAllowedFileOptions) => Promise<OpenedAllowedFile>;
38
42
  readonly statusOperation?: (operationId: string, waitMs: number | undefined, signal: AbortSignal) => Promise<ParseResult>;
package/dist/tools.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { CallToolRequestSchema, CancelTaskRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
3
+ import { selectDirectProfile, selectUrlProfile, } from "./capabilities.js";
3
4
  import { createClientRequestId as newClientRequestId, } from "./cube-client.js";
4
5
  import { OmniBridgeError } from "./errors.js";
5
6
  import { openAllowedFile, } from "./path-security.js";
6
7
  import { NOOP_PROGRESS } from "./progress.js";
7
- import { MACHINE_INSTRUCTIONS, cancelParseSchema, discardResultSchema, getParseStatusSchema, parseSchema, readResultSchema, } from "./protocol.js";
8
+ import { MACHINE_INSTRUCTIONS, cancelParseSchema, discardResultSchema, getParseStatusSchema, normalizeRepresentation, parseSchema, readResultSchema, } from "./protocol.js";
8
9
  import { discardResultOutputSchema, discardResultToolOutputSchema, parseResultSchema, parseToolOutputSchema, readResultOutputSchema, readResultToolOutputSchema, structuredResult, } from "./result-contract.js";
9
10
  import { classifySource } from "./source.js";
10
11
  import { TaskRuntime } from "./task-runtime.js";
@@ -137,9 +138,48 @@ async function parseLocal(args, clientRequestId, signal, progress, dependencies)
137
138
  }
138
139
  }
139
140
  }
141
+ // D2-D items 4-5: capability preflight for any explicit non-text request.
142
+ // The capability source must be available BEFORE any non-text grant/tool
143
+ // request is constructed: Cube requests use the authenticated
144
+ // `GET /api/omni-reader/capabilities/v1` preflight, remote MCP requests use
145
+ // the initialize result's exact `capabilities.experimental["cue.omni-reader"]`
146
+ // custom field. Without one exact compatible profile for the requested
147
+ // path/detail the request fails closed with UNSUPPORTED_DETAIL — no unknown
148
+ // fields are sent and there is never a silent text fallback.
149
+ async function preflightNonTextRepresentation(detail, sourceKind, signal, dependencies) {
150
+ if (sourceKind === "local") {
151
+ const capabilities = await dependencies.cubeClient.getCapabilities(signal);
152
+ selectDirectProfile(capabilities, detail);
153
+ }
154
+ else {
155
+ if (dependencies.remoteClient === undefined) {
156
+ throw bridgeError("REMOTE_PARSE_UNAVAILABLE", "Remote URL parsing is not available in this Bridge build.", { retryable: true });
157
+ }
158
+ const capabilities = await dependencies.remoteClient.initializeCapabilities(signal);
159
+ selectUrlProfile(capabilities, detail);
160
+ }
161
+ if (dependencies.operationManager === undefined) {
162
+ throw new OmniBridgeError({
163
+ code: "UNSUPPORTED_DETAIL",
164
+ message: "This Bridge build cannot deliver the requested output detail.",
165
+ failureScope: "bridge",
166
+ userAction: "Use plain Markdown output for this source.",
167
+ operationCreated: false,
168
+ fileUploaded: false,
169
+ parserStarted: false,
170
+ billed: false,
171
+ contentReleased: false,
172
+ retryable: false,
173
+ });
174
+ }
175
+ }
140
176
  async function parseValue(args, signal, progress, dependencies) {
141
177
  try {
142
178
  const source = classifySource(args.source);
179
+ const representation = normalizeRepresentation(args.detail);
180
+ if (representation.detail !== "text") {
181
+ await preflightNonTextRepresentation(representation.detail, source.kind, signal, dependencies);
182
+ }
143
183
  const clientRequestId = (dependencies.createClientRequestId ?? newClientRequestId)();
144
184
  if (dependencies.operationManager !== undefined) {
145
185
  const sourceHash = `sha256:${createHash("sha256")
@@ -171,6 +211,12 @@ async function parseValue(args, signal, progress, dependencies) {
171
211
  sourceFacts: {
172
212
  sourceHash,
173
213
  ...(sourceFingerprint === undefined ? {} : { sourceFingerprint }),
214
+ // D2-D item 3: the exact normalized representation fields enter
215
+ // SubmitOperationInput.sourceFacts BEFORE operationRequestHash so
216
+ // grounded/layout serialize to distinct identities. omitted/text
217
+ // must serialize byte-for-byte as legacy source facts so legacy
218
+ // journal identities stay reconstructable on upgrade.
219
+ ...(representation.detail === "text" ? {} : representation),
174
220
  },
175
221
  clientRequestId,
176
222
  signal,
@@ -263,6 +309,36 @@ async function callReadResult(resultId, cursor, maxBytes, dependencies) {
263
309
  });
264
310
  }
265
311
  catch (error) {
312
+ // D2-D Task 14: a retained logical bundle is only readable through a
313
+ // part-selecting v2 cursor, which the plain artifact read refuses with
314
+ // INVALID_RESULT_CURSOR. When the store exposes a named part reader, route
315
+ // the cursor there so clients can reassemble content and grounding
316
+ // independently from offset 0.
317
+ if (cursor !== undefined &&
318
+ error instanceof OmniBridgeError &&
319
+ error.code === "INVALID_RESULT_CURSOR" &&
320
+ dependencies.artifactStore.readBundlePart !== undefined) {
321
+ try {
322
+ const part = await dependencies.artifactStore.readBundlePart(resultId, cursor, maxBytes);
323
+ return structuredResult(readResultOutputSchema, {
324
+ status: "completed",
325
+ result: {
326
+ result_id: part.resultId,
327
+ part: part.part,
328
+ media_type: part.mediaType,
329
+ result_bytes: part.resultBytes,
330
+ offset: part.offset,
331
+ decoded_bytes: part.decodedBytes,
332
+ text: part.text,
333
+ expires_at: part.expiresAt,
334
+ ...(part.nextCursor === undefined ? {} : { next_cursor: part.nextCursor }),
335
+ },
336
+ });
337
+ }
338
+ catch (partError) {
339
+ return structuredResult(readResultOutputSchema, failed(partError));
340
+ }
341
+ }
266
342
  return structuredResult(readResultOutputSchema, failed(error));
267
343
  }
268
344
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueai/omni-reader-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Local stdio MCP bridge for direct Omni document parsing",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",