Skip to main content
Glama
elicitation.ts5.4 kB
import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { Logger } from './logger.js'; // Minimal helper to opportunistically use MCP Elicitation when available. // Safe across clients: validates schema shape, handles timeouts and -32601 fallbacks. export type PrimitiveSchema = | { type: 'string'; title?: string; description?: string; minLength?: number; maxLength?: number; pattern?: string; format?: 'email'|'uri'|'date'|'date-time'; default?: string } | { type: 'number'|'integer'; title?: string; description?: string; minimum?: number; maximum?: number; default?: number } | { type: 'boolean'; title?: string; description?: string; default?: boolean } | { type: 'string'; enum: string[]; enumNames?: string[]; title?: string; description?: string; default?: string }; export interface ElicitSchema { type: 'object'; properties: Record<string, PrimitiveSchema>; required?: string[]; } export interface ElicitOptions { timeoutMs?: number; fallback?: () => Promise<{ ok: boolean; value?: any; error?: string }>; } export function createElicitationHelper(server: Server, log: Logger) { // We do not require explicit capability detection: we optimistically try once // and disable on a Method-not-found (-32601) error for the session. let supported = true; // optimistic; will be set false on first failure const MIN_TIMEOUT_MS = 30_000; const MAX_TIMEOUT_MS = 10 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 3 * 60 * 1000; const timeoutEnvRaw = process.env.MCP_ELICITATION_TIMEOUT_MS ?? process.env.ELICITATION_TIMEOUT_MS ?? ''; const parsedEnvTimeout = Number.parseInt(timeoutEnvRaw, 10); const defaultTimeoutMs = Number.isFinite(parsedEnvTimeout) && parsedEnvTimeout > 0 ? Math.min(Math.max(parsedEnvTimeout, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS; if (timeoutEnvRaw) { log.debug('Configured elicitation timeout override detected', { defaultTimeoutMs, fromEnv: timeoutEnvRaw }); } function isSafeSchema(schema: ElicitSchema): boolean { if (!schema || schema.type !== 'object' || typeof schema.properties !== 'object') return false; const propertyEntries = Object.entries(schema.properties ?? {}); const propertyKeys = propertyEntries.map(([key]) => key); if (schema.required) { if (!Array.isArray(schema.required)) return false; const invalidRequired = schema.required.some((key) => typeof key !== 'string' || !propertyKeys.includes(key)); if (invalidRequired) return false; } return propertyEntries.every(([, rawSchema]) => { if (!rawSchema || typeof rawSchema !== 'object') return false; const primitive = rawSchema as PrimitiveSchema & { properties?: unknown; items?: unknown }; // narrow for guards if ('properties' in primitive || 'items' in primitive) return false; // nested schemas unsupported if (Array.isArray((primitive as any).enum)) { const enumValues = (primitive as any).enum; const allStrings = enumValues.every((value: unknown) => typeof value === 'string'); if (!allStrings) return false; return !('type' in primitive) || (primitive as any).type === 'string'; } if ((primitive as any).type === 'string') return true; if ((primitive as any).type === 'number' || (primitive as any).type === 'integer') return true; if ((primitive as any).type === 'boolean') return true; return false; }); } async function elicit(message: string, requestedSchema: ElicitSchema, opts: ElicitOptions = {}) { if (!supported || !isSafeSchema(requestedSchema)) { if (opts.fallback) return opts.fallback(); return { ok: false, error: 'elicitation-unsupported' }; } const params = { message, requestedSchema } as any; try { const elicitMethod = (server as any)?.elicitInput; if (typeof elicitMethod !== 'function') { supported = false; throw new Error('elicitInput-not-available'); } const requestedTimeout = opts.timeoutMs; const timeoutMs = Math.max(MIN_TIMEOUT_MS, requestedTimeout ?? defaultTimeoutMs); const res: any = await elicitMethod.call(server, params, { timeout: timeoutMs }); const action = res?.action; const content = res?.content; if (action === 'accept') return { ok: true, value: content }; if (action === 'decline' || action === 'cancel') { if (opts.fallback) return opts.fallback(); return { ok: false, error: action }; } if (opts.fallback) return opts.fallback(); return { ok: false, error: 'unexpected-response' }; } catch (e: any) { const msg = String(e?.message || e); const code = (e as any)?.code ?? (e as any)?.error?.code; // If client doesn't support it, don’t try again this session if ( msg.includes('Method not found') || msg.includes('elicitInput-not-available') || msg.includes('request-not-available') || String(code) === '-32601' ) { supported = false; } log.debug('Elicitation failed; falling back', { error: msg, code }); if (opts.fallback) return opts.fallback(); return { ok: false, error: msg.includes('timeout') ? 'timeout' : 'rpc-failed' }; } } return { supports: () => supported, elicit, getDefaultTimeoutMs: () => defaultTimeoutMs }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server