@cueai/omni-reader-mcp 1.0.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/README.md +49 -0
- package/dist/artifact-store.d.ts +65 -0
- package/dist/artifact-store.js +720 -0
- package/dist/cli/agent-config.d.ts +28 -0
- package/dist/cli/agent-config.js +335 -0
- package/dist/cli/clean.d.ts +9 -0
- package/dist/cli/clean.js +93 -0
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +157 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +59 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/cube-client.d.ts +48 -0
- package/dist/cube-client.js +161 -0
- package/dist/cursor.d.ts +14 -0
- package/dist/cursor.js +101 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +26 -0
- package/dist/iiis-client.d.ts +43 -0
- package/dist/iiis-client.js +694 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +174 -0
- package/dist/multipart-body.d.ts +12 -0
- package/dist/multipart-body.js +90 -0
- package/dist/operation-journal.d.ts +28 -0
- package/dist/operation-journal.js +351 -0
- package/dist/path-security.d.ts +22 -0
- package/dist/path-security.js +240 -0
- package/dist/progress.d.ts +4 -0
- package/dist/progress.js +3 -0
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.js +27 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +10 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +224 -0
- package/package.json +35 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type BigIntStats, type ReadStream } from "node:fs";
|
|
2
|
+
import { type FileHandle } from "node:fs/promises";
|
|
3
|
+
export interface PathSecurityFileSystem {
|
|
4
|
+
realpath(candidate: string): Promise<string>;
|
|
5
|
+
open(candidate: string, flags: number): Promise<FileHandle>;
|
|
6
|
+
stat(candidate: string): Promise<BigIntStats>;
|
|
7
|
+
}
|
|
8
|
+
export interface OpenAllowedFileOptions {
|
|
9
|
+
workspace: string;
|
|
10
|
+
extraRoots?: readonly string[];
|
|
11
|
+
homeDirectory?: string;
|
|
12
|
+
fileSystem?: PathSecurityFileSystem;
|
|
13
|
+
}
|
|
14
|
+
export interface OpenedAllowedFile {
|
|
15
|
+
readonly size: number;
|
|
16
|
+
readonly safeExtension: string;
|
|
17
|
+
readonly contentType: string;
|
|
18
|
+
createReadStream(): ReadStream;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function splitAllowedRoots(value: string | undefined): string[];
|
|
22
|
+
export declare function openAllowedFile(input: string, options: OpenAllowedFileOptions): Promise<OpenedAllowedFile>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { constants, } from "node:fs";
|
|
2
|
+
import { open as nodeOpen, realpath as nodeRealpath, stat as nodeStat, } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { MAX_FILE_BYTES } from "./constants.js";
|
|
6
|
+
import { OmniBridgeError } from "./errors.js";
|
|
7
|
+
const CONTENT_TYPES = {
|
|
8
|
+
".pdf": "application/pdf",
|
|
9
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
10
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
11
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
12
|
+
".doc": "application/msword",
|
|
13
|
+
".xls": "application/vnd.ms-excel",
|
|
14
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
15
|
+
".zip": "application/zip",
|
|
16
|
+
".jpg": "image/jpeg",
|
|
17
|
+
".jpeg": "image/jpeg",
|
|
18
|
+
".png": "image/png",
|
|
19
|
+
".gif": "image/gif",
|
|
20
|
+
".bmp": "image/bmp",
|
|
21
|
+
".tif": "image/tiff",
|
|
22
|
+
".tiff": "image/tiff",
|
|
23
|
+
".webp": "image/webp",
|
|
24
|
+
".heic": "image/heic",
|
|
25
|
+
".avif": "image/avif",
|
|
26
|
+
".wav": "audio/wav",
|
|
27
|
+
".mp3": "audio/mpeg",
|
|
28
|
+
".flac": "audio/flac",
|
|
29
|
+
".ogg": "audio/ogg",
|
|
30
|
+
".m4a": "audio/mp4",
|
|
31
|
+
".aac": "audio/aac",
|
|
32
|
+
".mp4": "video/mp4",
|
|
33
|
+
".mov": "video/quicktime",
|
|
34
|
+
".mkv": "video/x-matroska",
|
|
35
|
+
".webm": "video/webm",
|
|
36
|
+
".avi": "video/x-msvideo",
|
|
37
|
+
".m4v": "video/x-m4v",
|
|
38
|
+
".txt": "text/plain",
|
|
39
|
+
".md": "text/markdown",
|
|
40
|
+
".csv": "text/csv",
|
|
41
|
+
".html": "text/html",
|
|
42
|
+
".htm": "text/html",
|
|
43
|
+
};
|
|
44
|
+
const NODE_FILE_SYSTEM = {
|
|
45
|
+
realpath: (candidate) => nodeRealpath(candidate),
|
|
46
|
+
open: (candidate, flags) => nodeOpen(candidate, flags),
|
|
47
|
+
stat: (candidate) => nodeStat(candidate, { bigint: true }),
|
|
48
|
+
};
|
|
49
|
+
class DescriptorBackedFile {
|
|
50
|
+
size;
|
|
51
|
+
safeExtension;
|
|
52
|
+
contentType;
|
|
53
|
+
#handle;
|
|
54
|
+
#closed = false;
|
|
55
|
+
constructor(handle, size, safeExtension, contentType) {
|
|
56
|
+
this.size = size;
|
|
57
|
+
this.safeExtension = safeExtension;
|
|
58
|
+
this.contentType = contentType;
|
|
59
|
+
this.#handle = handle;
|
|
60
|
+
}
|
|
61
|
+
createReadStream() {
|
|
62
|
+
if (this.#closed) {
|
|
63
|
+
throw bridgeError("FILE_HANDLE_CLOSED", "The local file is no longer available for upload.", false);
|
|
64
|
+
}
|
|
65
|
+
return this.#handle.createReadStream({ autoClose: false, start: 0 });
|
|
66
|
+
}
|
|
67
|
+
async close() {
|
|
68
|
+
if (this.#closed) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.#closed = true;
|
|
72
|
+
await this.#handle.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function bridgeError(code, message, retryable) {
|
|
76
|
+
return new OmniBridgeError({
|
|
77
|
+
code,
|
|
78
|
+
message,
|
|
79
|
+
fileUploaded: false,
|
|
80
|
+
billed: false,
|
|
81
|
+
contentReleased: false,
|
|
82
|
+
retryable,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function validatePathText(value) {
|
|
86
|
+
if (value.length === 0 || value.includes("\0")) {
|
|
87
|
+
throw bridgeError("INVALID_LOCAL_PATH", "Provide one valid local file path.", false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function expandHomeOnly(value, homeDirectory) {
|
|
91
|
+
if (value === "~") {
|
|
92
|
+
return homeDirectory;
|
|
93
|
+
}
|
|
94
|
+
if (/^~[\\/]/u.test(value)) {
|
|
95
|
+
return path.join(homeDirectory, value.slice(2));
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function isInsideRoot(candidate, root) {
|
|
100
|
+
const relative = path.relative(root, candidate);
|
|
101
|
+
return (relative === "" ||
|
|
102
|
+
(relative !== ".." &&
|
|
103
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
104
|
+
!path.isAbsolute(relative)));
|
|
105
|
+
}
|
|
106
|
+
function requireInsideAnyRoot(candidate, roots) {
|
|
107
|
+
if (!roots.some((root) => isInsideRoot(candidate, root))) {
|
|
108
|
+
throw bridgeError("PATH_NOT_ALLOWED", "Move the file into the current workspace or add its parent directory to OMNI_ALLOWED_ROOTS.", false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function requireRegularFile(fileStat) {
|
|
112
|
+
if (!fileStat.isFile()) {
|
|
113
|
+
throw bridgeError("NOT_REGULAR_FILE", "The local path must identify one regular file.", false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function requireAllowedSize(fileStat) {
|
|
117
|
+
if (fileStat.size > BigInt(MAX_FILE_BYTES)) {
|
|
118
|
+
throw bridgeError("FILE_TOO_LARGE", "The local file exceeds the 256 MiB upload limit.", false);
|
|
119
|
+
}
|
|
120
|
+
return Number(fileStat.size);
|
|
121
|
+
}
|
|
122
|
+
function requireSameFile(before, after) {
|
|
123
|
+
if (before.dev !== after.dev ||
|
|
124
|
+
before.ino !== after.ino ||
|
|
125
|
+
before.size !== after.size ||
|
|
126
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
127
|
+
before.ctimeNs !== after.ctimeNs) {
|
|
128
|
+
throw bridgeError("FILE_CHANGED_DURING_OPEN", "The local file changed while it was being opened. Retry after file changes stop.", true);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function safeFileType(resolvedPath) {
|
|
132
|
+
const safeExtension = path.extname(resolvedPath).toLowerCase();
|
|
133
|
+
if (!/^\.[a-z0-9]{1,10}$/u.test(safeExtension)) {
|
|
134
|
+
throw bridgeError("UNSUPPORTED_FILE_TYPE", "The local file type is not supported.", false);
|
|
135
|
+
}
|
|
136
|
+
const contentType = CONTENT_TYPES[safeExtension];
|
|
137
|
+
if (contentType === undefined) {
|
|
138
|
+
throw bridgeError("UNSUPPORTED_FILE_TYPE", "The local file type is not supported.", false);
|
|
139
|
+
}
|
|
140
|
+
return { safeExtension, contentType };
|
|
141
|
+
}
|
|
142
|
+
function fileAccessError(error) {
|
|
143
|
+
if (error instanceof OmniBridgeError) {
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
const code = error?.code;
|
|
147
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
148
|
+
return bridgeError("FILE_NOT_FOUND", "The local file does not exist.", false);
|
|
149
|
+
}
|
|
150
|
+
if (code === "ENXIO" || code === "ENODEV" || code === "EISDIR") {
|
|
151
|
+
return bridgeError("NOT_REGULAR_FILE", "The local path must identify one regular file.", false);
|
|
152
|
+
}
|
|
153
|
+
if (code === "ELOOP") {
|
|
154
|
+
return bridgeError("FILE_CHANGED_DURING_OPEN", "The local file changed while it was being opened. Retry after file changes stop.", true);
|
|
155
|
+
}
|
|
156
|
+
return bridgeError("FILE_NOT_READABLE", "Omni cannot read the local file. Check its permissions and try again.", false);
|
|
157
|
+
}
|
|
158
|
+
async function resolveAllowedRoots(options, fileSystem, homeDirectory) {
|
|
159
|
+
validatePathText(options.workspace);
|
|
160
|
+
const workspaceInput = path.resolve(expandHomeOnly(options.workspace, homeDirectory));
|
|
161
|
+
const requestedRoots = [
|
|
162
|
+
workspaceInput,
|
|
163
|
+
...(options.extraRoots ?? [])
|
|
164
|
+
.filter((root) => root.length > 0)
|
|
165
|
+
.map((root) => {
|
|
166
|
+
validatePathText(root);
|
|
167
|
+
const expanded = expandHomeOnly(root, homeDirectory);
|
|
168
|
+
return path.isAbsolute(expanded)
|
|
169
|
+
? expanded
|
|
170
|
+
: path.resolve(workspaceInput, expanded);
|
|
171
|
+
}),
|
|
172
|
+
];
|
|
173
|
+
try {
|
|
174
|
+
const roots = await Promise.all(requestedRoots.map(async (root) => {
|
|
175
|
+
const resolved = await fileSystem.realpath(root);
|
|
176
|
+
const rootStat = await fileSystem.stat(resolved);
|
|
177
|
+
if (!rootStat.isDirectory()) {
|
|
178
|
+
throw bridgeError("INVALID_ALLOWED_ROOT", "The workspace and every OMNI_ALLOWED_ROOTS entry must be a directory.", false);
|
|
179
|
+
}
|
|
180
|
+
return resolved;
|
|
181
|
+
}));
|
|
182
|
+
return [...new Set(roots)];
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error instanceof OmniBridgeError) {
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
throw bridgeError("INVALID_ALLOWED_ROOT", "The workspace or an OMNI_ALLOWED_ROOTS entry is unavailable.", false);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export function splitAllowedRoots(value) {
|
|
192
|
+
if (value === undefined || value.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
return value.split(path.delimiter).filter((root) => root.length > 0);
|
|
196
|
+
}
|
|
197
|
+
export async function openAllowedFile(input, options) {
|
|
198
|
+
validatePathText(input);
|
|
199
|
+
const fileSystem = options.fileSystem ?? NODE_FILE_SYSTEM;
|
|
200
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
201
|
+
const roots = await resolveAllowedRoots(options, fileSystem, homeDirectory);
|
|
202
|
+
const expanded = expandHomeOnly(input, homeDirectory);
|
|
203
|
+
const candidate = path.isAbsolute(expanded)
|
|
204
|
+
? expanded
|
|
205
|
+
: path.resolve(options.workspace, expanded);
|
|
206
|
+
let resolved;
|
|
207
|
+
try {
|
|
208
|
+
resolved = await fileSystem.realpath(candidate);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
throw fileAccessError(error);
|
|
212
|
+
}
|
|
213
|
+
requireInsideAnyRoot(resolved, roots);
|
|
214
|
+
let handle;
|
|
215
|
+
try {
|
|
216
|
+
const pathStat = await fileSystem.stat(resolved);
|
|
217
|
+
requireRegularFile(pathStat);
|
|
218
|
+
const openFlags = constants.O_RDONLY |
|
|
219
|
+
(constants.O_NOFOLLOW ?? 0) |
|
|
220
|
+
(constants.O_NONBLOCK ?? 0);
|
|
221
|
+
handle = await fileSystem.open(resolved, openFlags);
|
|
222
|
+
const before = await handle.stat({ bigint: true });
|
|
223
|
+
requireRegularFile(before);
|
|
224
|
+
requireSameFile(pathStat, before);
|
|
225
|
+
const size = requireAllowedSize(before);
|
|
226
|
+
const afterPath = await fileSystem.realpath(resolved);
|
|
227
|
+
requireInsideAnyRoot(afterPath, roots);
|
|
228
|
+
const after = await fileSystem.stat(afterPath);
|
|
229
|
+
requireRegularFile(after);
|
|
230
|
+
requireSameFile(before, after);
|
|
231
|
+
const { safeExtension, contentType } = safeFileType(afterPath);
|
|
232
|
+
const opened = new DescriptorBackedFile(handle, size, safeExtension, contentType);
|
|
233
|
+
handle = undefined;
|
|
234
|
+
return opened;
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
await handle?.close().catch(() => undefined);
|
|
238
|
+
throw fileAccessError(error);
|
|
239
|
+
}
|
|
240
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const LOCAL_BRIDGE_PROTOCOL_VERSION = "omni.local_bridge_tools.v1";
|
|
3
|
+
export declare const SAFETY_INSTRUCTIONS: string;
|
|
4
|
+
export declare const parseDocumentSchema: z.ZodObject<{
|
|
5
|
+
local_path: z.ZodEffects<z.ZodString, string, string>;
|
|
6
|
+
no_store: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
output: z.ZodDefault<z.ZodLiteral<"markdown">>;
|
|
8
|
+
}, "strict", z.ZodTypeAny, {
|
|
9
|
+
local_path: string;
|
|
10
|
+
no_store: boolean;
|
|
11
|
+
output: "markdown";
|
|
12
|
+
}, {
|
|
13
|
+
local_path: string;
|
|
14
|
+
no_store?: boolean | undefined;
|
|
15
|
+
output?: "markdown" | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export declare const readResultSchema: z.ZodObject<{
|
|
18
|
+
result_id: z.ZodString;
|
|
19
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
20
|
+
max_bytes: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
}, "strict", z.ZodTypeAny, {
|
|
22
|
+
result_id: string;
|
|
23
|
+
max_bytes?: number | undefined;
|
|
24
|
+
cursor?: string | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
result_id: string;
|
|
27
|
+
max_bytes?: number | undefined;
|
|
28
|
+
cursor?: string | undefined;
|
|
29
|
+
}>;
|
|
30
|
+
export declare const discardResultSchema: z.ZodObject<{
|
|
31
|
+
result_id: z.ZodString;
|
|
32
|
+
}, "strict", z.ZodTypeAny, {
|
|
33
|
+
result_id: string;
|
|
34
|
+
}, {
|
|
35
|
+
result_id: string;
|
|
36
|
+
}>;
|
|
37
|
+
export type ParseDocumentArguments = z.infer<typeof parseDocumentSchema>;
|
|
38
|
+
export type ReadResultArguments = z.infer<typeof readResultSchema>;
|
|
39
|
+
export type DiscardResultArguments = z.infer<typeof discardResultSchema>;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RESULT_CHUNK_MAX_BYTES } from "./constants.js";
|
|
3
|
+
export const LOCAL_BRIDGE_PROTOCOL_VERSION = "omni.local_bridge_tools.v1";
|
|
4
|
+
export const SAFETY_INSTRUCTIONS = "For local documents, pass the exact user-provided path to parse_document.\n" +
|
|
5
|
+
"Do not read, attach, base64-encode, or insert the file content into the\n" +
|
|
6
|
+
"conversation before calling this tool. Do not use @file as a prerequisite.\n" +
|
|
7
|
+
"Use parse_url for HTTP(S) URLs.";
|
|
8
|
+
const resultIdSchema = z.string().regex(/^result_[A-Za-z0-9_-]{16,64}$/u);
|
|
9
|
+
export const parseDocumentSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
local_path: z.string().min(1).refine((value) => !value.includes("\0")),
|
|
12
|
+
no_store: z.boolean().default(true),
|
|
13
|
+
output: z.literal("markdown").default("markdown"),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
export const readResultSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
result_id: resultIdSchema,
|
|
19
|
+
cursor: z.string().min(1).max(2048).optional(),
|
|
20
|
+
max_bytes: z.number().int().min(1).max(RESULT_CHUNK_MAX_BYTES).optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
export const discardResultSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
result_id: resultIdSchema,
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { SAFETY_INSTRUCTIONS } from "./protocol.js";
|
|
3
|
+
import { registerOmniTools } from "./tools.js";
|
|
4
|
+
const SERVER_NAME = "omni-reader-mcp";
|
|
5
|
+
const SERVER_VERSION = "1.0.0";
|
|
6
|
+
export function createOmniMcpServer(dependencies) {
|
|
7
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: SAFETY_INSTRUCTIONS });
|
|
8
|
+
registerOmniTools(server, dependencies);
|
|
9
|
+
return server;
|
|
10
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ArtifactReadResult, LocalResult } from "./artifact-store.js";
|
|
3
|
+
import { type CubeGrantClient } from "./cube-client.js";
|
|
4
|
+
import type { IiisClient, ResultRetentionSink } from "./iiis-client.js";
|
|
5
|
+
import { type OpenAllowedFileOptions, type OpenedAllowedFile } from "./path-security.js";
|
|
6
|
+
interface ToolRetention extends ResultRetentionSink {
|
|
7
|
+
result(): LocalResult;
|
|
8
|
+
}
|
|
9
|
+
interface ToolArtifactStore {
|
|
10
|
+
createRetention(): ToolRetention;
|
|
11
|
+
read(resultId: string, cursor?: string, maxBytes?: number): Promise<ArtifactReadResult>;
|
|
12
|
+
discard(resultId: string): Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
export interface OmniToolDependencies {
|
|
15
|
+
readonly workspace: string;
|
|
16
|
+
readonly extraRoots?: readonly string[];
|
|
17
|
+
readonly cubeClient: Pick<CubeGrantClient, "createGrant">;
|
|
18
|
+
readonly iiisClient: Pick<IiisClient, "uploadAndWait" | "ack">;
|
|
19
|
+
readonly artifactStore: ToolArtifactStore;
|
|
20
|
+
readonly createClientRequestId?: () => string;
|
|
21
|
+
readonly openFile?: (localPath: string, options: OpenAllowedFileOptions) => Promise<OpenedAllowedFile>;
|
|
22
|
+
}
|
|
23
|
+
export declare function registerOmniTools(server: McpServer, dependencies: OmniToolDependencies): void;
|
|
24
|
+
export {};
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { createClientRequestId as newClientRequestId, } from "./cube-client.js";
|
|
3
|
+
import { OmniBridgeError } from "./errors.js";
|
|
4
|
+
import { openAllowedFile, } from "./path-security.js";
|
|
5
|
+
import { NOOP_PROGRESS } from "./progress.js";
|
|
6
|
+
import { SAFETY_INSTRUCTIONS, discardResultSchema, parseDocumentSchema, readResultSchema, } from "./protocol.js";
|
|
7
|
+
function bridgeError(code, message) {
|
|
8
|
+
return new OmniBridgeError({
|
|
9
|
+
code,
|
|
10
|
+
message,
|
|
11
|
+
fileUploaded: false,
|
|
12
|
+
billed: false,
|
|
13
|
+
contentReleased: false,
|
|
14
|
+
retryable: false,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function errorResult(error) {
|
|
18
|
+
const stable = error instanceof OmniBridgeError
|
|
19
|
+
? error
|
|
20
|
+
: bridgeError("BRIDGE_INTERNAL_ERROR", "The local Omni Bridge could not complete the request.");
|
|
21
|
+
return {
|
|
22
|
+
isError: true,
|
|
23
|
+
content: [{ type: "text", text: JSON.stringify(stable.toJSON()) }],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function successResult(value) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function progressSink(extra) {
|
|
32
|
+
const progressToken = extra._meta?.progressToken;
|
|
33
|
+
if (progressToken === undefined)
|
|
34
|
+
return NOOP_PROGRESS;
|
|
35
|
+
return {
|
|
36
|
+
async report(progress, total, message) {
|
|
37
|
+
await extra.sendNotification({
|
|
38
|
+
method: "notifications/progress",
|
|
39
|
+
params: { progressToken, progress, total, message },
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function parseSuccess(local) {
|
|
45
|
+
if (local.kind === "inline") {
|
|
46
|
+
return successResult({
|
|
47
|
+
operation_id: local.operationId,
|
|
48
|
+
text: local.text,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (local.nextCursor === undefined) {
|
|
52
|
+
throw bridgeError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact cursor is missing.");
|
|
53
|
+
}
|
|
54
|
+
return successResult({
|
|
55
|
+
result_id: local.resultId,
|
|
56
|
+
result_bytes: local.resultBytes,
|
|
57
|
+
artifact_path: local.artifactPath,
|
|
58
|
+
expires_at: local.expiresAt,
|
|
59
|
+
preview: local.preview,
|
|
60
|
+
next_cursor: local.nextCursor,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function parseDocument(args, extra, dependencies) {
|
|
64
|
+
const openFile = dependencies.openFile ?? openAllowedFile;
|
|
65
|
+
const opened = await openFile(args.local_path, {
|
|
66
|
+
workspace: dependencies.workspace,
|
|
67
|
+
extraRoots: dependencies.extraRoots,
|
|
68
|
+
});
|
|
69
|
+
let primaryError;
|
|
70
|
+
try {
|
|
71
|
+
const clientRequestId = (dependencies.createClientRequestId ?? newClientRequestId)();
|
|
72
|
+
const granted = await dependencies.cubeClient.createGrant({
|
|
73
|
+
contentLength: opened.size,
|
|
74
|
+
contentType: opened.contentType,
|
|
75
|
+
fileExtension: opened.safeExtension,
|
|
76
|
+
noStore: args.no_store,
|
|
77
|
+
output: args.output,
|
|
78
|
+
}, clientRequestId, extra.signal);
|
|
79
|
+
const retention = dependencies.artifactStore.createRetention();
|
|
80
|
+
const operation = {
|
|
81
|
+
parseGrant: granted.parseGrant,
|
|
82
|
+
operationId: granted.operationId,
|
|
83
|
+
operationToken: granted.operationToken,
|
|
84
|
+
uploadUrl: granted.uploadUrl,
|
|
85
|
+
expiresAt: granted.expiresAt,
|
|
86
|
+
openedFile: opened,
|
|
87
|
+
retention,
|
|
88
|
+
progress: progressSink(extra),
|
|
89
|
+
signal: extra.signal,
|
|
90
|
+
};
|
|
91
|
+
await dependencies.iiisClient.uploadAndWait(operation);
|
|
92
|
+
const local = retention.result();
|
|
93
|
+
const result = parseSuccess(local);
|
|
94
|
+
await dependencies.iiisClient.ack(operation);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
primaryError = error;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
try {
|
|
103
|
+
await opened.close();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
if (primaryError === undefined) {
|
|
107
|
+
throw bridgeError("FILE_CLOSE_FAILED", "The local source file could not be closed safely.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function readSuccess(result) {
|
|
113
|
+
return successResult({
|
|
114
|
+
result_id: result.resultId,
|
|
115
|
+
result_bytes: result.resultBytes,
|
|
116
|
+
expires_at: result.expiresAt,
|
|
117
|
+
text: result.text,
|
|
118
|
+
...(result.nextCursor === undefined ? {} : { next_cursor: result.nextCursor }),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function invalidArgumentsResult() {
|
|
122
|
+
return errorResult(bridgeError("INVALID_TOOL_ARGUMENTS", "The Omni tool arguments are invalid."));
|
|
123
|
+
}
|
|
124
|
+
async function dispatchTool(name, rawArguments, extra, dependencies) {
|
|
125
|
+
if (name === "parse_document") {
|
|
126
|
+
const parsed = parseDocumentSchema.safeParse(rawArguments ?? {});
|
|
127
|
+
if (!parsed.success)
|
|
128
|
+
return invalidArgumentsResult();
|
|
129
|
+
try {
|
|
130
|
+
return await parseDocument(parsed.data, extra, dependencies);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return errorResult(error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (name === "read_result") {
|
|
137
|
+
const parsed = readResultSchema.safeParse(rawArguments ?? {});
|
|
138
|
+
if (!parsed.success)
|
|
139
|
+
return invalidArgumentsResult();
|
|
140
|
+
try {
|
|
141
|
+
return readSuccess(await dependencies.artifactStore.read(parsed.data.result_id, parsed.data.cursor, parsed.data.max_bytes));
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return errorResult(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (name === "discard_result") {
|
|
148
|
+
const parsed = discardResultSchema.safeParse(rawArguments ?? {});
|
|
149
|
+
if (!parsed.success)
|
|
150
|
+
return invalidArgumentsResult();
|
|
151
|
+
try {
|
|
152
|
+
return successResult({
|
|
153
|
+
result_id: parsed.data.result_id,
|
|
154
|
+
discarded: await dependencies.artifactStore.discard(parsed.data.result_id),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return errorResult(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return errorResult(bridgeError("TOOL_NOT_FOUND", "The requested Omni tool is not available."));
|
|
162
|
+
}
|
|
163
|
+
export function registerOmniTools(server, dependencies) {
|
|
164
|
+
server.registerTool("parse_document", {
|
|
165
|
+
description: `${SAFETY_INSTRUCTIONS}\n\nParse one local file through Omni without sending source bytes through the model or Cube-MCP.`,
|
|
166
|
+
inputSchema: parseDocumentSchema,
|
|
167
|
+
annotations: {
|
|
168
|
+
readOnlyHint: false,
|
|
169
|
+
destructiveHint: false,
|
|
170
|
+
idempotentHint: false,
|
|
171
|
+
openWorldHint: true,
|
|
172
|
+
},
|
|
173
|
+
}, async (args, extra) => {
|
|
174
|
+
try {
|
|
175
|
+
return await parseDocument(args, extra, dependencies);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return errorResult(error);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
server.registerTool("read_result", {
|
|
182
|
+
description: "Read one UTF-8 chunk from a Bridge-created local result artifact.",
|
|
183
|
+
inputSchema: readResultSchema,
|
|
184
|
+
annotations: {
|
|
185
|
+
readOnlyHint: true,
|
|
186
|
+
destructiveHint: false,
|
|
187
|
+
idempotentHint: true,
|
|
188
|
+
openWorldHint: false,
|
|
189
|
+
},
|
|
190
|
+
}, async (args) => {
|
|
191
|
+
try {
|
|
192
|
+
return readSuccess(await dependencies.artifactStore.read(args.result_id, args.cursor, args.max_bytes));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return errorResult(error);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
server.registerTool("discard_result", {
|
|
199
|
+
description: "Immediately delete one Bridge-created local result artifact.",
|
|
200
|
+
inputSchema: discardResultSchema,
|
|
201
|
+
annotations: {
|
|
202
|
+
readOnlyHint: false,
|
|
203
|
+
destructiveHint: true,
|
|
204
|
+
idempotentHint: true,
|
|
205
|
+
openWorldHint: false,
|
|
206
|
+
},
|
|
207
|
+
}, async (args) => {
|
|
208
|
+
try {
|
|
209
|
+
return successResult({
|
|
210
|
+
result_id: args.result_id,
|
|
211
|
+
discarded: await dependencies.artifactStore.discard(args.result_id),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return errorResult(error);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
219
|
+
if (request.params.task !== undefined) {
|
|
220
|
+
return errorResult(bridgeError("TOOL_TASK_UNSUPPORTED", "Task-augmented tool calls are not supported by this Omni Bridge."));
|
|
221
|
+
}
|
|
222
|
+
return dispatchTool(request.params.name, request.params.arguments, extra, dependencies);
|
|
223
|
+
});
|
|
224
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cueai/omni-reader-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local stdio MCP bridge for direct Omni document parsing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.12"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"omni-reader-mcp": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
24
|
+
"prepack": "npm run test && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
28
|
+
"zod": "^3.25.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.17.0",
|
|
32
|
+
"typescript": "^5.8.0",
|
|
33
|
+
"vitest": "^4.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|