everything-mcp-server 0.0.0-stage → 0.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/src/agent.js ADDED
@@ -0,0 +1,92 @@
1
+ import { normalizeRoot, configFromEnv, composeQuery, filetimeToISO, resultPath, relativePath, logicalBytes, HttpTransport } from './everything.js';
2
+ import { inspectWindows, discoverIpc } from './discovery.js';
3
+ import { AgentError, errorResult, searchSchema } from './schema.js';
4
+ import { validateQuery, validateRoot } from './validation.js';
5
+
6
+ const metadata = [
7
+ ['size', 'size_bytes', logicalBytes],
8
+ ['date_modified', 'modified_utc', filetimeToISO],
9
+ ['date_created', 'created_utc', filetimeToISO],
10
+ ['date_accessed', 'accessed_utc', filetimeToISO],
11
+ ['attributes', 'attributes', logicalBytes],
12
+ ['run_count', 'run_count', logicalBytes],
13
+ ];
14
+
15
+ export class AgentClient {
16
+ constructor(env = process.env, dependencies = {}) {
17
+ try { this.config = configFromEnv(env); }
18
+ catch (error) { this.configError = error; this.config = configFromEnv({}); }
19
+ this.env = env;
20
+ this.dependencies = dependencies;
21
+ }
22
+ async select(signal) {
23
+ if (this.configError) throw this.configError;
24
+ if (this.env.EVERYTHING_URL) {
25
+ const transport = new HttpTransport(this.config, this.dependencies.fetch);
26
+ return { transport, info: { transport: 'http', instance: this.env.EVERYTHING_INSTANCE ?? null, version: null, local: this.config.local, url: this.config.url } };
27
+ }
28
+ const inventory = this.env.EVERYTHING_INSTANCE !== undefined ? { instances: [] } : await (this.dependencies.inspect ?? inspectWindows)(this.env, signal);
29
+ return (this.dependencies.discoverIpc ?? discoverIpc)(inventory, this.config, this.env, signal);
30
+ }
31
+ async ensureSelected(signal) {
32
+ // Pin a successful selection; retry discovery after a failed connection.
33
+ this.pending ??= this.select(signal).catch(error => { this.pending = null; throw error; });
34
+ return this.pending;
35
+ }
36
+ async search(arguments_, signal) {
37
+ const input = searchSchema.parse(arguments_);
38
+ const deadline = AbortSignal.timeout(input.timeout_ms ?? this.config.timeoutMs);
39
+ const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
40
+ try {
41
+ const functions = await validateQuery(input);
42
+ const sumSize = input.count === 0 && input.fields.includes('size');
43
+ if (sumSize && input.kind !== 'file') throw new AgentError('INVALID_ARGUMENT', 'Use kind:"file" with count:0 and fields:["size"] to sum matching file bytes. Folder sizes are available in result rows.');
44
+ if (input.root) input.root = normalizeRoot(input.root);
45
+ const { transport, info } = await this.ensureSelected(combined);
46
+ if (input.root && info.local) await validateRoot(input.root, Math.min(input.timeout_ms ?? this.config.timeoutMs, 3000), combined);
47
+ if (functions.includes('dupe') && !info.version?.startsWith('1.5.')) {
48
+ throw new AgentError('UNSUPPORTED_QUERY', 'dupe: requires a verified Everything 1.5 backend', false, 'Use IPC with Everything 1.5 to verify the query-language version.');
49
+ }
50
+ const count = Math.min(input.count, this.config.maxResults);
51
+ const query = composeQuery(input);
52
+ const data = await transport.request(input, count, query, combined);
53
+ if (!count) return { total: data.totalResults, ...(sumSize ? { total_size_bytes: data.totalResults ? logicalBytes(await transport.totalSize(input, query, combined)) : 0, size_basis: 'indexed_logical' } : {}) };
54
+ if (!data.results.length && input.offset < data.totalResults) throw new AgentError('INCOMPLETE_PAGE', 'Repeat the query or narrow its scope to obtain the remaining results.', true);
55
+ const columns = metadata.filter(([field]) => input.fields.includes(field));
56
+ const rows = [];
57
+ for (const row of data.results) {
58
+ if (!['file', 'folder'].includes(row?.type) || typeof row.name !== 'string' || typeof row.path !== 'string') throw new AgentError('INVALID_RESPONSE', 'Expected an indexed file or folder path');
59
+ const full = resultPath(row);
60
+ const filename = input.root ? relativePath(input.root, full) : full;
61
+ let size = row.size;
62
+ if (input.fields.includes('size') && row.type === 'folder' &&
63
+ (info.version?.startsWith('1.4.') || (info.transport === 'http' && (size == null || size === '' || String(size) === '0')))) {
64
+ size = await transport.directorySize(full, input, combined);
65
+ }
66
+ rows.push([filename.replaceAll('\\', '/'), row.type, ...columns.map(([field, , format]) => format(field === 'size' ? size : row[field]))]);
67
+ }
68
+ return {
69
+ ...(input.root ? { base: input.root.replaceAll('\\', '/') } : {}),
70
+ total: data.totalResults, next_offset: input.offset + rows.length < data.totalResults ? input.offset + rows.length : null,
71
+ columns: ['path', 'type', ...columns.map(([, column]) => column)], rows,
72
+ ...(count < input.count ? { limited_by: ['result_cap'] } : {}),
73
+ ...(input.fields.includes('size') ? { size_basis: 'indexed_logical' } : {}),
74
+ };
75
+ } catch (error) {
76
+ if (combined.aborted) throw new AgentError(signal?.aborted ? 'CANCELLED' : 'TIMEOUT', signal?.aborted ? 'Request cancelled' : 'Search reached its time budget; narrow the scope or increase the timeout.', true);
77
+ throw error;
78
+ }
79
+ }
80
+ async status(signal, verbose = false) {
81
+ try {
82
+ const { transport, info } = await this.ensureSelected(signal);
83
+ const result = await transport.request({}, 0, '', signal);
84
+ return { ready: true, transport: info.transport, version: info.version, instance: info.instance,
85
+ capabilities: ['paths', 'count', 'browse', 'batch', 'file_size', 'folder_size', 'total_file_size', 'modified_time', 'regex', ...(info.transport === 'ipc' ? ['created_time', 'accessed_time', 'attributes', 'run_count'] : []), ...(info.version?.startsWith('1.5.') ? ['dupe'] : [])], problems: [],
86
+ ...(verbose ? { details: { total_indexed: result.totalResults, backend: info.url ?? 'local IPC', limits: { max_results: this.config.maxResults, output_bytes: this.config.outputBytes } } } : {}) };
87
+ } catch (error) {
88
+ if (signal?.aborted) throw error;
89
+ return { ready: false, transport: null, version: null, instance: null, capabilities: [], problems: [errorResult(error).error] };
90
+ }
91
+ }
92
+ }
package/src/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { AgentClient } from './agent.js';
4
+ import { errorResult, AgentError, diagnosticSchema } from './schema.js';
5
+ import { compactSearch } from './compact.js';
6
+
7
+ try {
8
+ const [command = 'mcp', ...rest] = process.argv.slice(2);
9
+ if (command === 'mcp') {
10
+ if (rest.length) throw new AgentError('INVALID_ARGUMENT', 'Run mcp as a standalone command');
11
+ await (await import('./server.js')).startServer();
12
+ } else {
13
+ let raw = '{}';
14
+ if (rest[0] === '--input' && rest.length === 2) raw = await readFile(rest[1], 'utf8');
15
+ else if (rest[0] === '--stdin' && rest.length === 1) {
16
+ const chunks = []; let bytes = 0;
17
+ for await (const chunk of process.stdin) {
18
+ bytes += chunk.length;
19
+ if (bytes > 1048576) throw new AgentError('INVALID_ARGUMENT', 'stdin exceeds 1 MiB');
20
+ chunks.push(chunk);
21
+ }
22
+ raw = Buffer.concat(chunks).toString('utf8');
23
+ } else if (rest.length === 1) raw = rest[0];
24
+ else if (rest.length) throw new AgentError('INVALID_ARGUMENT', 'Use JSON, --input file.json or --stdin.');
25
+ let args;
26
+ try { args = JSON.parse(raw.replace(/^\uFEFF/, '')); }
27
+ catch { throw new AgentError('INVALID_ARGUMENT', 'Input must be valid JSON'); }
28
+ const client = new AgentClient();
29
+ let result;
30
+ if (command === 'search') result = await compactSearch(client, args);
31
+ else if (command === 'doctor') { diagnosticSchema.parse(args); result = await client.status(undefined, true); }
32
+ else if (command === 'help') result = { commands: ['mcp', 'doctor', 'search'], input: 'JSON | --input file.json | --stdin', example: { requests: [{ query: 'ext:pdf', count: 10 }] } };
33
+ else throw new AgentError('INVALID_ARGUMENT', `Choose mcp, doctor or search; received ${command}.`);
34
+ console.log(JSON.stringify(result));
35
+ if (result.ready === false || result.results?.every(item => item.error)) process.exitCode = 1;
36
+ }
37
+ } catch (error) { console.log(JSON.stringify(errorResult(error))); process.exitCode = 1; }
package/src/compact.js ADDED
@@ -0,0 +1,64 @@
1
+ import { AgentError, errorResult, requestsSchema } from './schema.js';
2
+
3
+ export async function compactSearch(client, arguments_ = {}, signal) {
4
+ const { requests } = requestsSchema.parse(arguments_);
5
+ if (requests.reduce((sum, request) => sum + Math.min(request.count, client.config.maxResults), 0) > client.config.maxResults) {
6
+ throw new AgentError('INVALID_ARGUMENT', 'Batch aggregate count exceeds the configured result cap');
7
+ }
8
+ const results = new Array(requests.length);
9
+ const budget = Math.floor((client.config.outputBytes - 512) / requests.length);
10
+ let next = 0;
11
+ const worker = async () => {
12
+ while (next < requests.length) {
13
+ const index = next++;
14
+ const request = requests[index];
15
+ try {
16
+ const result = await client.search(request, signal);
17
+ const returned = result.rows?.length ?? 0;
18
+ if (result.rows?.length && Buffer.byteLength(JSON.stringify(result)) > budget) {
19
+ const allRows = result.rows;
20
+ const originalLimitedBy = result.limited_by ?? [];
21
+ const candidate = (rowCount) => {
22
+ result.rows = allRows.slice(0, rowCount);
23
+ result.next_offset = request.offset + rowCount < result.total ? request.offset + rowCount : null;
24
+ result.limited_by = [...new Set([...originalLimitedBy, 'output_bytes'])];
25
+ return Buffer.byteLength(JSON.stringify(result)) <= budget;
26
+ };
27
+
28
+ let low = 1;
29
+ let high = allRows.length - 1;
30
+ let best = 0;
31
+ while (low <= high) {
32
+ const mid = (low + high) >> 1;
33
+ if (candidate(mid)) {
34
+ best = mid;
35
+ low = mid + 1;
36
+ } else {
37
+ high = mid - 1;
38
+ }
39
+ }
40
+ if (best > 0) {
41
+ candidate(best);
42
+ } else {
43
+ result.rows = [];
44
+ result.next_offset = request.offset < result.total ? request.offset : null;
45
+ result.limited_by = [...new Set([...originalLimitedBy, 'output_bytes'])];
46
+ }
47
+ }
48
+ if (Buffer.byteLength(JSON.stringify(result)) > budget || (returned && !result.rows.length)) {
49
+ throw new AgentError('OUTPUT_TOO_LARGE', 'Reduce fields or batch size to fit the output budget.');
50
+ }
51
+ results[index] = result;
52
+ } catch (error) {
53
+ if (signal?.aborted) throw error;
54
+ results[index] = { error: errorResult(error).error };
55
+ }
56
+ }
57
+ };
58
+ await Promise.all([worker(), worker()]);
59
+ const output = { results };
60
+ if (Buffer.byteLength(JSON.stringify(output)) > client.config.outputBytes) {
61
+ throw new AgentError('OUTPUT_TOO_LARGE', 'Reduce batch size to fit the output budget.');
62
+ }
63
+ return output;
64
+ }
@@ -0,0 +1,47 @@
1
+ import path from 'node:path';
2
+ import { access } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { AgentError } from './schema.js';
5
+ import { IpcTransport } from './ipc.js';
6
+
7
+ const project = fileURLToPath(new URL('../', import.meta.url));
8
+
9
+ export async function inspectWindows(env = process.env, _signal) {
10
+ if (process.platform !== 'win32') throw new AgentError('PLATFORM_SELECTION', 'Use Windows for local IPC, or configure EVERYTHING_URL.');
11
+ return { instances: [] };
12
+ }
13
+
14
+ export async function discoverIpc(discovery, config, env = process.env, signal, factory = options => new IpcTransport(options)) {
15
+ const arch = { x64: 'x64', ia32: 'x86', arm64: 'ARM64', arm: 'ARM' }[process.arch];
16
+ const executable = env.EVERYTHING_ES_PATH || path.join(project, 'vendor', 'es', arch || process.arch, 'es.exe');
17
+ try { await access(executable); }
18
+ catch { throw new AgentError('HELPER_NOT_FOUND', 'Install the bundled ES helper, or set EVERYTHING_ES_PATH to ES 1.1.0.38 or newer.'); }
19
+
20
+ const explicit = env.EVERYTHING_INSTANCE !== undefined;
21
+ const instances = explicit ? [env.EVERYTHING_INSTANCE] : [...new Set([...(discovery?.instances ?? []), '', '1.5a', '1.5'])];
22
+ const candidates = [];
23
+
24
+ for (const instance of instances) {
25
+ if (signal?.aborted) throw new AgentError('CANCELLED', 'Request cancelled', true);
26
+ const transport = factory({ executable, instance, timeoutMs: config.timeoutMs, responseBytes: config.responseBytes });
27
+ try {
28
+ const version = await transport.getVersion(signal);
29
+ const [major, minor, revision] = version.split('.').map(Number);
30
+ if (major < 1 || (major === 1 && (minor < 4 || (minor === 4 && revision < 1)))) {
31
+ throw new AgentError('UNSUPPORTED_VERSION', 'Everything 1.4.1 or newer is required');
32
+ }
33
+ await transport.request({ timeout_ms: 2500 }, 0, '', signal);
34
+ candidates.push({ transport, info: { transport: 'ipc', instance, version, local: true } });
35
+ } catch (error) {
36
+ if (signal?.aborted || error.code === 'CANCELLED' || explicit) throw error;
37
+ }
38
+ }
39
+
40
+ if (candidates.length > 1) {
41
+ throw new AgentError('MULTIPLE_INSTANCES', 'Multiple active Everything instances detected: ' + candidates.map(c => JSON.stringify(c.info.instance)).join(', '), false, 'Set EVERYTHING_INSTANCE to select the intended instance.');
42
+ }
43
+ if (!candidates.length) {
44
+ throw new AgentError('INSTANCE_NOT_FOUND', 'Start Everything and ensure its index is ready, or configure EVERYTHING_URL.', true);
45
+ }
46
+ return candidates[0];
47
+ }
@@ -0,0 +1,166 @@
1
+ import path from 'node:path';
2
+ import { AgentError } from './schema.js';
3
+
4
+ const integerEnv = (env, name, fallback, min, max) => {
5
+ if (env[name] === undefined || env[name] === '') return fallback;
6
+ const n = Number(env[name]);
7
+ if (!Number.isInteger(n) || n < min || n > max) throw new AgentError('INVALID_CONFIG', `${name} must be an integer between ${min} and ${max}`);
8
+ return n;
9
+ };
10
+ export function configFromEnv(env = process.env) {
11
+ let url;
12
+ try { url = new URL(env.EVERYTHING_URL || 'http://127.0.0.1/'); }
13
+ catch { throw new AgentError('INVALID_CONFIG', 'EVERYTHING_URL must be an HTTP(S) origin'); }
14
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
15
+ throw new AgentError('INVALID_CONFIG', 'Use an HTTP(S) origin without credentials, path, query or fragment; use EVERYTHING_USERNAME/PASSWORD for authentication');
16
+ }
17
+ return {
18
+ url: url.href,
19
+ username: env.EVERYTHING_USERNAME || '', password: env.EVERYTHING_PASSWORD || '',
20
+ maxResults: integerEnv(env, 'EVERYTHING_MAX_RESULTS', 1000, 1, 1000),
21
+ outputBytes: integerEnv(env, 'EVERYTHING_OUTPUT_BYTES', 16384, 4096, 65536),
22
+ responseBytes: integerEnv(env, 'EVERYTHING_RESPONSE_BYTES', 2097152, 4096, 16777216),
23
+ timeoutMs: integerEnv(env, 'EVERYTHING_TIMEOUT_MS', 15000, 100, 120000),
24
+ local: ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname),
25
+ };
26
+ }
27
+
28
+ export function normalizeRoot(root) {
29
+ const value = root.replaceAll('/', '\\');
30
+ // Reject search operators and relative/device paths before embedding in query syntax.
31
+ if (!/^(?:[a-z]:\\|\\\\[^\\]+\\[^\\]+(?:\\|$))/i.test(value) || /["<>|*?\x00-\x1f]/.test(value) || value.startsWith('\\\\.\\') || value.startsWith('\\\\?\\') || /:(?!\\)/.test(value)) {
32
+ throw new AgentError('INVALID_ARGUMENT', 'root must be an absolute Windows drive or UNC directory without wildcards or query operators');
33
+ }
34
+ const normalized = path.win32.normalize(value);
35
+ return normalized.length > path.win32.parse(normalized).root.length ? normalized.replace(/\\+$/, '') : normalized;
36
+ }
37
+
38
+ export function composeQuery(input) {
39
+ const filters = [];
40
+ if (input.root) {
41
+ const root = normalizeRoot(input.root);
42
+ filters.push(input.recursive ? `path:"${root.endsWith('\\') ? root : root + '\\'}"` : `parent:"${root}"`);
43
+ }
44
+ if (input.kind !== 'any') filters.push(`${input.kind}:`);
45
+ let query = input.query;
46
+ // HTTP regex=1 disables all native filters. Use a scoped modifier instead.
47
+ if (input.regex && query) query = `regex:"${query.replaceAll('"', '\\x22')}"`;
48
+ if (query) filters.push(filters.length ? `<${query}>` : query);
49
+ return filters.join(' ');
50
+ }
51
+
52
+ export function filetimeToISO(value) {
53
+ if (value === undefined || value === null || value === '' || String(value) === '0') return null;
54
+ try {
55
+ // BigInt avoids rounding away 100-nanosecond Windows FILETIME ticks.
56
+ const milliseconds = BigInt(value) / 10000n - 11644473600000n;
57
+ if (milliseconds < -8640000000000000n || milliseconds > 8640000000000000n) return null;
58
+ return new Date(Number(milliseconds)).toISOString();
59
+ } catch { return null; }
60
+ }
61
+
62
+ export function resultPath(row) {
63
+ // Everything represents indexed drive roots as name="C:", path="".
64
+ if (row.path === '' && /^[a-z]:$/i.test(row.name)) return `${row.name}\\`;
65
+ const fullPath = path.win32.join(row.path, row.name);
66
+ if (!/^(?:[a-z]:\\|\\\\[^\\]+\\[^\\]+)/i.test(fullPath)) throw new AgentError('INVALID_RESPONSE', 'Everything returned a non-absolute result path');
67
+ return fullPath;
68
+ }
69
+
70
+ export const syntax = {
71
+ query_language: 'Everything native syntax; regex:true treats query as a regular expression pattern',
72
+ examples: [
73
+ { query: 'ext:pdf;docx report', purpose: 'Documents with report in filename' },
74
+ { query: 'size:>100mb dm:thismonth', purpose: 'Large recently modified files' },
75
+ { query: '<ext:ts | ext:js> !path:node_modules !path:.git', root: 'D:\\Projects', purpose: 'Source files beneath a directory' },
76
+ { query: '^IMG_\\d+\\.jpg$', regex: true, purpose: 'Filename regex' },
77
+ { query: 'ext:log', kind: 'file', count: 0, fields: ['size'], purpose: 'Count matching files and sum their indexed logical bytes' },
78
+ ],
79
+ operators: { and: 'space', or: '|', not: '!', grouping: '<...>', literal: '"..."', wildcard: '* ?', extension: 'ext:ts;js', size: 'size:>10mb or size:1kb..1mb', modified: 'dm:today / dm:thisweek / dm:2026-01-01..2026-12-31', created: 'dc:thismonth', direct_children: 'parent:"D:\\Projects"' },
80
+ notes: ['Empty query matches all; omitted root searches the selected index.', 'Use regex:true with a regular expression pattern; root/kind filters stay active.', 'Directory exclusions use !path:node_modules.', 'Results and pagination use a live index. File and folder sizes use indexed logical bytes.'],
81
+ docs: ['https://www.voidtools.com/support/everything/searching/', 'https://www.voidtools.com/support/everything/http/'],
82
+ };
83
+
84
+ export function logicalBytes(value) {
85
+ if (!/^\d+$/.test(String(value))) return null;
86
+ const bytes = BigInt(value);
87
+ return bytes <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bytes) : bytes.toString();
88
+ }
89
+ export function relativePath(root, full) {
90
+ const relative = path.win32.relative(root, full);
91
+ if (relative === '..' || relative.startsWith('..\\') || path.win32.isAbsolute(relative)) throw new AgentError('INVALID_RESPONSE', 'Expected a result within root');
92
+ return relative || '.';
93
+ }
94
+
95
+ export class HttpTransport {
96
+ constructor(config, fetchImpl = fetch) {
97
+ this.config = config;
98
+ this.fetch = fetchImpl;
99
+ }
100
+ async request(input, count, query, signal) {
101
+ if (input.sort && !['name', 'path', 'size', 'date_modified'].includes(input.sort)) throw new AgentError('UNSUPPORTED_QUERY', `Use IPC for sort:${input.sort}; HTTP sorts by name, path, size or date_modified.`);
102
+ const url = new URL(this.config.url);
103
+ const params = {
104
+ search: query, json: 1, count, offset: input.offset ?? 0,
105
+ sort: input.sort ?? 'path', ascending: input.ascending === false ? 0 : 1,
106
+ path_column: count > 0 ? 1 : 0, size_column: input.fields?.includes('size') ? 1 : 0,
107
+ date_modified_column: input.fields?.includes('date_modified') ? 1 : 0,
108
+ regex: 0, case: 0, wholeword: 0, path: 0, diacritics: 0, prefix: 0, suffix: 0,
109
+ ignore_punctuation: 0, ignore_whitespace: 0,
110
+ };
111
+ for (const field of input.fields ?? []) params[`${field}_column`] = 1;
112
+ for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));
113
+ const deadline = AbortSignal.timeout(input.timeout_ms ?? this.config.timeoutMs);
114
+ const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
115
+ const headers = { Accept: 'application/json' };
116
+ if (this.config.username || this.config.password) headers.Authorization = 'Basic ' + Buffer.from(this.config.username + ':' + this.config.password).toString('base64');
117
+ try {
118
+ if (combined.aborted) throw combined.reason;
119
+ const response = await this.fetch(url, { headers, signal: combined, redirect: 'error' });
120
+ if (!response.ok) {
121
+ await response.body?.cancel();
122
+ if ([401, 403].includes(response.status)) throw new AgentError('AUTH_REQUIRED', 'Set EVERYTHING_USERNAME and EVERYTHING_PASSWORD for this HTTP service.');
123
+ throw new AgentError('HTTP_ERROR', 'Everything returned HTTP ' + response.status, response.status >= 500);
124
+ }
125
+ if (!response.body) throw new AgentError('INVALID_RESPONSE', 'Expected a JSON response body');
126
+ const chunks = []; let bytes = 0;
127
+ for await (const chunk of response.body) {
128
+ bytes += chunk.byteLength;
129
+ if (bytes > this.config.responseBytes) throw new AgentError('RESPONSE_TOO_LARGE', 'Reduce count or fields to fit the backend response budget.');
130
+ chunks.push(chunk);
131
+ }
132
+ let data;
133
+ try { data = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
134
+ catch { throw new AgentError('INVALID_RESPONSE', 'Expected JSON from the configured Everything HTTP service'); }
135
+ if (!Number.isSafeInteger(data?.totalResults) || data.totalResults < 0 || !Array.isArray(data.results) || data.results.length > count) throw new AgentError('INVALID_RESPONSE', 'Expected a valid search result envelope');
136
+ for (const field of (input.fields ?? []).filter(field => !['size', 'date_modified'].includes(field))) {
137
+ if (data.results.some(row => !(field in row))) throw new AgentError('UNSUPPORTED_METADATA', `Use IPC for ${field}; the selected HTTP service provides other metadata columns.`);
138
+ }
139
+ return data;
140
+ } catch (error) {
141
+ if (combined.aborted) throw new AgentError(signal?.aborted ? 'CANCELLED' : 'TIMEOUT', signal?.aborted ? 'Request cancelled' : 'HTTP request reached its time budget', true);
142
+ if (error instanceof AgentError) throw error;
143
+ throw new AgentError('CONNECTION_FAILED', 'Check the configured Everything HTTP URL and service availability.', true);
144
+ }
145
+ }
146
+ async directorySize(root, input, signal) {
147
+ // HTTP 1.4 may omit folder sizes; sum only the requested folder's indexed files.
148
+ const scoped = { root, query: '', recursive: true, kind: 'file', fields: ['size'], timeout_ms: input.timeout_ms };
149
+ return this.totalSize(scoped, composeQuery(scoped), signal);
150
+ }
151
+ async totalSize(input, query, signal) {
152
+ let offset = 0, sum = 0n;
153
+ for (;;) {
154
+ const page = await this.request({ ...input, fields: ['size'], offset }, this.config.maxResults, query, signal);
155
+ if (!page.results.length && offset < page.totalResults) throw new AgentError('INCOMPLETE_PAGE', 'Repeat the size query to obtain the remaining files.', true);
156
+ for (const row of page.results) {
157
+ if (row.type !== 'file' || typeof row.name !== 'string' || typeof row.path !== 'string') throw new AgentError('INVALID_RESPONSE', 'Expected indexed files for a size sum');
158
+ if (input.root) relativePath(input.root, resultPath(row));
159
+ if (logicalBytes(row.size) === null) throw new AgentError('METADATA_PENDING', 'Wait for indexed file sizes, then repeat the query.', true);
160
+ sum += BigInt(row.size);
161
+ }
162
+ offset += page.results.length;
163
+ if (offset >= page.totalResults) return sum.toString();
164
+ }
165
+ }
166
+ }
package/src/ipc.js ADDED
@@ -0,0 +1,82 @@
1
+ import path from 'node:path';
2
+ import { runProcess } from './process.js';
3
+ import { AgentError } from './schema.js';
4
+ import { composeQuery } from './everything.js';
5
+
6
+ export function parseCSV(text) {
7
+ const rows = []; let row = [], cell = '', quoted = false;
8
+ for (let i = 0; i < text.length; i++) {
9
+ const c = text[i];
10
+ if (c === '"') { if (quoted && text[i + 1] === '"') { cell += '"'; i++; } else quoted = !quoted; }
11
+ else if (c === ',' && !quoted) { row.push(cell); cell = ''; }
12
+ else if ((c === '\n' || c === '\r') && !quoted) { if (c === '\r' && text[i+1] === '\n') i++; row.push(cell); if (row.some(Boolean)) rows.push(row); row = []; cell = ''; }
13
+ else cell += c;
14
+ }
15
+ if (quoted) throw new AgentError('INVALID_RESPONSE', 'Unterminated CSV field from ES');
16
+ if (cell || row.length) { row.push(cell); rows.push(row); }
17
+ return rows;
18
+ }
19
+
20
+ export class IpcTransport {
21
+ constructor({ executable, instance = '', timeoutMs = 15000, responseBytes = 2097152 }, runner = runProcess) {
22
+ Object.assign(this, { executable, instance, timeoutMs, responseBytes, runner });
23
+ }
24
+ async call(args, signal, timeoutMs = this.timeoutMs) {
25
+ const result = await this.runner(this.executable, ['-argv', '-code-page', '65001', '-instance', this.instance, ...args], { signal, timeoutMs, maxBytes: this.responseBytes });
26
+ if (result.code !== 0) throw new AgentError(result.code === 8 ? 'INSTANCE_NOT_FOUND' : result.code === 6 ? 'ES_VERSION_UNSUPPORTED' : 'IPC_QUERY_FAILED', `ES IPC request failed (exit ${result.code})`, true, 'Run everything-mcp-server doctor; check the instance, index readiness, helper version and process permissions.');
27
+ return result.stdout;
28
+ }
29
+ async getVersion(signal) {
30
+ const value = (await this.call(['-get-everything-version'], signal, 2500)).trim();
31
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(value) || value.startsWith('0.')) throw new AgentError('INSTANCE_NOT_FOUND', 'Choose an active Everything client instance');
32
+ return value;
33
+ }
34
+ options(input) {
35
+ const args = ['-timeout', String(input.timeout_ms ?? this.timeoutMs), '-count', '4294967295', '-no-pause', '-no-highlight', '-no-hide-empty-search-results', '-no-empty-search-help', '-header', '-no-footer'];
36
+ for (const flag of ['case','whole-word','match-path','diacritics','prefix','suffix','ignore-punctuation','ignore-whitespace']) args.push(`-no-${flag}`);
37
+ args.push('-sort', `${(input.sort ?? 'path').replaceAll('_', '-')}-${input.ascending === false ? 'descending' : 'ascending'}`);
38
+ return args;
39
+ }
40
+ async request(input, count, query, signal) {
41
+ const deadline = AbortSignal.timeout(input.timeout_ms ?? this.timeoutMs);
42
+ try { return await this.execute(input,count,query,signal ? AbortSignal.any([signal,deadline]) : deadline); }
43
+ catch(error) { if (deadline.aborted && !signal?.aborted) throw new AgentError('TIMEOUT','IPC query reached its time budget',true,'Narrow the query or increase timeout_ms.'); throw error; }
44
+ }
45
+ async execute(input, count, query, signal) {
46
+ const common = this.options(input);
47
+ const text = await this.call([...common, '-viewport-offset','0','-viewport-count','0','-get-result-count','--',query],signal,input.timeout_ms);
48
+ if (!/^\d+\s*$/.test(text)) throw new AgentError('INVALID_RESPONSE', 'ES returned an invalid count');
49
+ const totalResults = Number(text.trim());
50
+ if (!Number.isSafeInteger(totalResults)) throw new AgentError('INVALID_RESPONSE', 'ES count exceeds the safe integer range');
51
+ if (!count) return { totalResults, results: [] };
52
+ const fields = [...new Set(['attributes', ...(input.fields ?? [])])];
53
+ const csv = await this.call([...common, '-columns', ['filename', ...fields.map(field => field.replaceAll('_', '-'))].join(';'),
54
+ '-csv','-no-folder-append-path-separator','-no-digit-grouping','-size-format','1','-date-format','2',
55
+ '-viewport-offset',String(input.offset ?? 0),'-viewport-count',String(count),'--',query],signal,input.timeout_ms);
56
+ const [header = [], ...rows] = parseCSV(csv);
57
+ const filename = header.indexOf('Filename'), attributes = header.indexOf('Attributes');
58
+ const indexes = fields.map(field => [field, header.indexOf(field.split('_').map(word => word[0].toUpperCase() + word.slice(1)).join(' '))]);
59
+ if (filename < 0 || indexes.some(([, index]) => index < 0) || rows.length > count) throw new AgentError('INVALID_RESPONSE', 'ES returned unexpected CSV columns or too many rows');
60
+ const results = rows.map(row => {
61
+ if (row.length !== header.length) throw new AgentError('INVALID_RESPONSE', 'ES returned a malformed CSV row');
62
+ const attr = row[attributes];
63
+ if (!/^(?:\d+|[A-Z]*)$/.test(attr)) throw new AgentError('INVALID_RESPONSE', 'ES returned invalid attributes');
64
+ const full = row[filename], folder = /^\d+$/.test(attr) ? Boolean(BigInt(attr) & 16n) : attr.includes('D');
65
+ const drive = /^[a-z]:\\?$/i.test(full);
66
+ return { name: drive ? full.slice(0,2) : path.win32.basename(full), path: drive ? '' : path.win32.dirname(full), type: folder ? 'folder' : 'file', ...Object.fromEntries(indexes.map(([field, index]) => [field, row[index]])) };
67
+ });
68
+ return { totalResults, results };
69
+ }
70
+ async totalSize(input, query, signal) {
71
+ const sum = (await this.call([...this.options(input), '-size-format', '1', '-no-digit-grouping', '-viewport-offset', '0', '-viewport-count', '4294967295', '-get-total-size', '--', query], signal, input.timeout_ms)).trim();
72
+ if (!/^\d+$/.test(sum)) throw new AgentError('INVALID_RESPONSE', 'Expected an integer byte count from ES');
73
+ if (sum === '18446744073709551615') {
74
+ if ((await this.request(input, 0, query, signal)).totalResults === 0) return '0';
75
+ throw new AgentError('METADATA_PENDING', 'Wait for indexed file sizes, then repeat the query.', true);
76
+ }
77
+ return sum;
78
+ }
79
+ async directorySize(root, input, signal) {
80
+ return this.totalSize(input, composeQuery({ root, query: '', recursive: true, kind: 'file', regex: false }), signal);
81
+ }
82
+ }
package/src/process.js ADDED
@@ -0,0 +1,22 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { AgentError } from './schema.js';
3
+
4
+ export function runProcess(executable, args, { signal, timeoutMs = 15000, maxBytes = 2097152, env } = {}) {
5
+ return new Promise((resolve, reject) => {
6
+ if (signal?.aborted) return reject(new AgentError('CANCELLED', 'Request cancelled', true));
7
+ let child, timer, ended = false, size = 0;
8
+ const chunks = [], errors = [], cleanup = new AbortController();
9
+ const finish = (error, result) => {
10
+ if (ended) return; ended = true; clearTimeout(timer); cleanup.abort();
11
+ if (error) { child?.kill(); reject(error); } else resolve(result);
12
+ };
13
+ try { child = spawn(executable, args, { shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], ...(env ? {env} : {}) }); }
14
+ catch { return finish(new AgentError('PROCESS_START_FAILED', 'Install the backend helper and check execution permissions')); }
15
+ timer = setTimeout(() => finish(new AgentError('TIMEOUT', 'Backend helper timed out', true)), timeoutMs);
16
+ signal?.addEventListener('abort', () => finish(new AgentError('CANCELLED', 'Request cancelled', true)), { once: true, signal: cleanup.signal });
17
+ child.on('error', error => finish(new AgentError(error.code === 'ENOENT' ? 'HELPER_NOT_FOUND' : 'PROCESS_START_FAILED', 'Install the backend helper and check execution permissions')));
18
+ child.stdout.on('data', chunk => { size += chunk.length; if (size > maxBytes) finish(new AgentError('RESPONSE_TOO_LARGE', 'Backend output exceeded the configured byte limit')); else chunks.push(chunk); });
19
+ child.stderr.on('data', chunk => { if (errors.reduce((n,b)=>n+b.length,0) < 8192) errors.push(chunk); });
20
+ child.on('close', code => finish(null, { code, stdout: Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/, ''), stderr: Buffer.concat(errors).toString('utf8') }));
21
+ });
22
+ }
package/src/schema.js ADDED
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod';
2
+
3
+ export const requestSchema = z.object({
4
+ query: z.string().max(8192).default('').describe('Everything filename, path or indexed metadata query (e.g. *.pdf, size:>10mb).'),
5
+ root: z.string().max(1024).optional().describe('Absolute Windows directory or drive path; omitted = global search.'),
6
+ recursive: z.boolean().default(true).describe('When root is set: search all descendants (true) or direct children only (false).'),
7
+ kind: z.enum(['any', 'file', 'folder']).default('any').describe('Filter by entry type (any, file, folder).'),
8
+ regex: z.boolean().default(false).describe('Treat query as a regular expression.'),
9
+ count: z.number().int().min(0).max(1000).default(20).describe('Maximum rows; 0 counts. With kind:file and fields:[size], also sums matched file bytes.'),
10
+ offset: z.number().int().min(0).max(4294967295).default(0).describe('Zero-based result offset for pagination.'),
11
+ sort: z.enum(['name', 'path', 'size', 'extension', 'date_modified', 'date_created', 'date_accessed', 'attributes', 'run_count']).default('path').describe('Sort field. HTTP supports name/path/size/date_modified; IPC supports all.'),
12
+ ascending: z.boolean().default(true).describe('Sort ascending (true) or descending (false).'),
13
+ fields: z.array(z.enum(['size', 'date_modified', 'date_created', 'date_accessed', 'attributes', 'run_count'])).max(6).default([]).describe('Optional metadata columns to retrieve.'),
14
+ }).strict();
15
+
16
+ export const searchSchema = requestSchema.extend({
17
+ timeout_ms: z.number().int().min(100).max(120000).optional().describe('Search timeout in milliseconds.'),
18
+ });
19
+
20
+ export const requestsSchema = z.object({
21
+ requests: z.array(requestSchema).min(1).max(8).describe('1–8 queries, in result order.'),
22
+ }).strict();
23
+
24
+ export const diagnosticSchema = z.object({ verbose: z.boolean().default(false) }).strict();
25
+
26
+ const uint = z.number().int().nonnegative();
27
+
28
+ export const errorSchema = z.object({
29
+ code: z.string(),
30
+ message: z.string(),
31
+ retryable: z.boolean(),
32
+ hint: z.string().optional(),
33
+ });
34
+
35
+ export const searchOutputSchema = z.object({
36
+ results: z.array(z.union([
37
+ z.object({
38
+ total: uint,
39
+ next_offset: uint.nullable(),
40
+ base: z.string().optional(),
41
+ columns: z.array(z.string()),
42
+ rows: z.array(z.array(z.union([z.string(), z.number(), z.null()]))),
43
+ limited_by: z.array(z.string()).optional(),
44
+ size_basis: z.literal('indexed_logical').optional(),
45
+ }).strict(),
46
+ z.object({
47
+ total: uint,
48
+ total_size_bytes: z.union([uint, z.string()]).optional(),
49
+ size_basis: z.literal('indexed_logical').optional(),
50
+ }).strict(),
51
+ z.object({ error: errorSchema }).strict(),
52
+ ])),
53
+ }).strict();
54
+
55
+ export const statusOutputSchema = z.object({
56
+ ready: z.boolean(),
57
+ transport: z.string().nullable(),
58
+ version: z.string().nullable(),
59
+ instance: z.string().nullable(),
60
+ capabilities: z.array(z.string()),
61
+ problems: z.array(errorSchema),
62
+ details: z.record(z.string(), z.unknown()).optional(),
63
+ }).strict();
64
+
65
+ export class AgentError extends Error {
66
+ constructor(code, message, retryable = false, hint) {
67
+ super(message);
68
+ Object.assign(this, { code, retryable, hint });
69
+ }
70
+ }
71
+
72
+ export function errorResult(error) {
73
+ if (error instanceof z.ZodError) {
74
+ return {
75
+ ok: false,
76
+ error: {
77
+ code: 'INVALID_ARGUMENT',
78
+ message: error.issues.map(issue => `${issue.path.join('.') || 'arguments'}: ${issue.message}`).join('; '),
79
+ retryable: false,
80
+ },
81
+ };
82
+ }
83
+ return {
84
+ ok: false,
85
+ error: {
86
+ code: error instanceof AgentError ? error.code : 'INTERNAL_ERROR',
87
+ message: error instanceof AgentError ? error.message : 'Unexpected internal error',
88
+ retryable: error.retryable ?? false,
89
+ ...(error.hint ? { hint: error.hint } : {}),
90
+ },
91
+ };
92
+ }