@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
200 lines • 7.91 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { MAX_PAGES, TIMEOUT } from "../core/mcp/constants.js";
import { abortable } from "../utils/abort.js";
import { VERSION } from "../version.js";
import { Accessor } from "./base.js";
/** Where a failed MCP session end or client close is reported; silent until a host sets `debug`. */
export const logger = {};
/**
* Whether the server read the request and refused it, which says nothing
* against the connection: a JSON-RPC error outside the codes the SDK spends
* on transport, timeout and session faults. Mirrors Python's `answered`.
*/
export async function answered(error) {
const { INVALID_PARAMS, METHOD_NOT_FOUND, ProtocolError } = await import('@modelcontextprotocol/client');
if (!(error instanceof ProtocolError))
return false;
const code = error.code;
return code === INVALID_PARAMS || code === METHOD_NOT_FOUND || code < -32768 || code > -32000;
}
/** Whether the SDK gave up waiting for the server's answer. Mirrors Python's `timed_out`. */
export async function timedOut(error) {
const { SdkError, SdkErrorCode } = await import('@modelcontextprotocol/client');
return error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout;
}
/**
* A streamable-HTTP MCP server, one client kept per install.
*
* The client opens on first use and stays open until the CLI is removed. A
* call that fails drops it, unless the server answered it with an error: the
* next call reopens one, and the dropped one closes once the calls running on
* it end. A cancelled call leaves it open. Unlike Python, every caller shares it: each request's
* fetch goes through its caller's pool, so its rule and policies judge it.
* The SDK loads on the first request, so building or restoring an MCP CLI
* never imports it.
*/
export class MCPAccessor extends Accessor {
config;
opening = null;
calls = new Map();
dropped = new Set();
closing = new Set();
constructor(config) {
super();
this.config = config;
}
/** The one header `auth` asks for; none without auth. */
headers() {
const auth = this.config.auth;
if (auth === undefined || auth === null)
return {};
if (auth.type === 'bearer')
return { Authorization: `Bearer ${auth.token}` };
return { [auth.name ?? '']: auth.token };
}
/** An initialized client, which the caller closes. */
async connect(signal) {
const { Client, StreamableHTTPClientTransport } = await import('@modelcontextprotocol/client');
const transport = new StreamableHTTPClientTransport(new URL(this.config.url), {
requestInit: { headers: this.headers() },
});
const client = new Client({ name: 'mirage', version: VERSION }, { listMaxPages: MAX_PAGES });
try {
await client.connect(transport, { timeout: TIMEOUT * 1000, ...(signal ? { signal } : {}) });
}
catch (error) {
await client.close();
throw error;
}
return client;
}
/**
* End the server's MCP session, waiting at most the call timeout, then close
* the client, which aborts a session end still in flight.
*/
async shut(client) {
let timer;
try {
await Promise.race([
client.transport?.terminateSession(),
new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error('the server did not end the session in time'));
}, TIMEOUT * 1000);
}),
]);
}
catch (error) {
logger.debug?.(`MCP session end failed: ${String(error)}`);
}
finally {
clearTimeout(timer);
}
await client.close();
}
/** Shut `opening` in the background, kept in `closing` so `close` joins it. */
shutting(opening) {
const done = opening
.then((client) => this.shut(client))
.catch((error) => {
logger.debug?.(`MCP client close failed: ${String(error)}`);
})
.finally(() => {
this.closing.delete(done);
});
this.closing.add(done);
return done;
}
/** Run `use` on an initialized client, closed when it settles. */
async session(use, signal) {
const client = await this.connect(signal);
try {
return await use(client);
}
finally {
await this.shut(client);
}
}
/** The kept client's opening, started when there is none. */
kept() {
if (this.opening === null) {
const opening = this.connect();
this.opening = opening;
opening.catch(() => {
if (this.opening === opening)
this.opening = null;
});
}
return this.opening;
}
/** Stop handing out `opening`; it closes once the calls running on it end. */
drop(opening) {
if (this.opening === opening)
this.opening = null;
this.dropped.add(opening);
}
/**
* Count a call off `opening`, and start closing it if dropped and that was
* its last; the call does not wait for the close, which may hang.
*/
release(opening) {
const left = (this.calls.get(opening) ?? 1) - 1;
if (left > 0) {
this.calls.set(opening, left);
return;
}
this.calls.delete(opening);
if (this.dropped.delete(opening))
void this.shutting(opening);
}
/** The server's title and every tool it lists; the SDK reads every page. */
async listTools() {
return this.session(async (client) => {
const { tools } = await client.listTools({}, { timeout: TIMEOUT * 1000 });
const info = client.getServerVersion();
if (info === undefined)
return ['', tools];
return [info.title === undefined || info.title === '' ? info.name : info.title, tools];
});
}
/** Call one tool on the kept client, abandoned when `signal` aborts. */
async callTool(name, args, signal) {
const opening = this.kept();
this.calls.set(opening, (this.calls.get(opening) ?? 0) + 1);
try {
const client = await abortable(opening, signal);
return await client.callTool({ name, arguments: args }, { timeout: TIMEOUT * 1000, ...(signal ? { signal } : {}) });
}
catch (error) {
if (signal?.aborted !== true && !(await answered(error)))
this.drop(opening);
throw error;
}
finally {
this.release(opening);
}
}
/** Close the kept client and any dropped one. */
async close() {
const openings = [...(this.opening === null ? [] : [this.opening]), ...this.dropped];
this.opening = null;
this.dropped.clear();
for (const opening of openings)
void this.shutting(opening);
while (this.closing.size > 0)
await Promise.allSettled([...this.closing]);
}
}
//# sourceMappingURL=mcp.js.map