Source index
src/context.mjs
const PROTOCOL = '2025-03-26';
export function parseRpc(body, contentType = '') {
if (!contentType.includes('text/event-stream')) return JSON.parse(body);
const messages = body.split(/\r?\n\r?\n/).map(block =>
block.split(/\r?\n/).filter(line => line.startsWith('data:')).map(line => line.slice(5).trim()).join('\n')
).filter(Boolean).map(data => JSON.parse(data));
const response = messages.findLast(message => message.result !== undefined || message.error);
if (!response) throw new Error('Context returned no JSON-RPC result.');
return response;
}
export class ContextClient {
constructor({url = process.env.SANITY_CONTEXT_URL, token = process.env.SANITY_CONTEXT_TOKEN, fetchImpl = fetch} = {}) {
const endpoint = new URL(url || 'https://invalid.example');
if (endpoint.protocol !== 'https:' || endpoint.hostname !== 'api.sanity.io' || endpoint.username || endpoint.password || !endpoint.pathname.startsWith('/v1/context/organizations/'))
throw new Error('Set an official HTTPS Sanity Context MCP endpoint.');
if (!token) throw new Error('Set SANITY_CONTEXT_TOKEN in the server environment.');
this.url = endpoint.href; this.token = token; this.fetch = fetchImpl; this.id = 0;
}
async rpc(method, params, notification = false) {
const headers = {'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', Authorization: `Bearer ${this.token}`, 'MCP-Protocol-Version': PROTOCOL};
if (this.session) headers['Mcp-Session-Id'] = this.session;
const response = await this.fetch(this.url, {
method: 'POST', headers, signal: AbortSignal.timeout(45000),
body: JSON.stringify({jsonrpc: '2.0', ...(notification ? {} : {id: ++this.id}), method, ...(params ? {params} : {})}),
});
if (!response.ok) {
let detail = '';
try {
const error = await response.json();
const message = error.error?.message || error.message;
if (typeof message === 'string') detail = message.split(this.token).join('[credential omitted]').replace(/Bearer\s+\S+/gi, 'Bearer [omitted]').slice(0, 500);
} catch {}
throw new Error(`Sanity Context HTTP ${response.status}${detail ? ': ' + detail : '; response omitted'}.`);
}
if (response.headers.get('mcp-session-id')) this.session = response.headers.get('mcp-session-id');
const body = await response.text();
if (notification || !body.trim()) return;
const data = parseRpc(body, response.headers.get('content-type') || '');
if (data.error) throw new Error(`Sanity Context JSON-RPC ${data.error.code}: ${String(data.error.message).split(this.token).join('[credential omitted]').replace(/Bearer\s+\S+/gi, 'Bearer [omitted]').slice(0, 300)}`);
return data.result;
}
async connect() {
const info = await this.rpc('initialize', {protocolVersion: PROTOCOL, capabilities: {}, clientInfo: {name: 'access-atlas', version: '0.1.0'}});
await this.rpc('notifications/initialized', {}, true);
const listed = await this.rpc('tools/list');
this.tools = listed.tools || [];
if (!this.tools.length) throw new Error('Context MCP has no tools. Check deployed schema and endpoint sources.');
return {server: info.serverInfo, tools: this.tools};
}
async call(name, args) {
if (!this.tools.some(tool => tool.name === name)) throw new Error('Tool is not in the read-only Context tool list.');
const result = await this.rpc('tools/call', {name, arguments: args});
return result;
}
}