feat: add support for MCP Sampling as AI provider (#863)
* feat: support MCP sampling * support provider registry * use standard config options for MCP provider * update fastmcp to support passing params to requestSampling * move key name definition to base provider * moved check for required api key to provider class * remove unused code * more cleanup * more cleanup * refactor provider * remove not needed files * more cleanup * more cleanup * more cleanup * update docs * fix tests * add tests * format fix * clean files * merge fixes * format fix * feat: add support for MCP Sampling as AI provider * initial mcp ai sdk * fix references to old provider * update models * lint * fix gemini-cli conflicts * ran format * Update src/provider-registry/index.js Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> * fix circular dependency Circular Dependency Issue ✅ FIXED Root Cause: BaseAIProvider was importing from index.js, which includes commands.js and other modules that eventually import back to AI providers Solution: Changed imports to use direct paths to avoid circular dependencies: Updated base-provider.js to import log directly from utils.js Updated gemini-cli.js to import log directly from utils.js Result: Fixed 11 failing tests in mcp-provider.test.js * fix gemini test * fix(claude-code): recover from CLI JSON truncation bug (#913) (#920) Gracefully handle SyntaxError thrown by @anthropic-ai/claude-code when the CLI truncates large JSON outputs (4–16 kB cut-offs).\n\nKey points:\n• Detect JSON parse error + existing buffered text in both doGenerate() and doStream() code paths.\n• Convert the failure into a recoverable 'truncated' finish state and push a provider-warning.\n• Allows Task Master to continue parsing long PRDs / expand-task operations instead of crashing.\n\nA patch changeset (.changeset/claude-code-json-truncation.md) is included for the next release.\n\nRef: eyaltoledano/claude-task-master#913 * docs: fix gemini-cli authentication documentation (#923) Remove erroneous 'gemini auth login' command references and replace with correct 'gemini' command authentication flow. Update documentation to reflect proper OAuth setup process via the gemini CLI interactive interface. * fix tests * fix: update ai-sdk-provider-gemini-cli to 0.0.4 for improved authentication (#932) - Fixed authentication compatibility issues with Google auth - Added support for 'api-key' auth type alongside 'gemini-api-key' - Resolved "Unsupported authType: undefined" runtime errors - Updated @google/gemini-cli-core dependency to 0.1.9 - Improved documentation and removed invalid auth references - Maintained backward compatibility while enhancing type validation * call logging directly Need to patch upstream fastmcp to allow easier access and bootstrap the TM mcp logger to use the fastmcp logger which today is only exposed in the tools handler * fix tests * removing logs until we figure out how to pass mcp logger * format * fix tests * format * clean up * cleanup * readme fix --------- Co-authored-by: Oren Melamed <oren.m@gloat.com> Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Co-authored-by: Ben Vargas <ben@vargas.com>
This commit is contained in:
106
mcp-server/src/custom-sdk/errors.js
Normal file
106
mcp-server/src/custom-sdk/errors.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* src/ai-providers/custom-sdk/mcp/errors.js
|
||||
*
|
||||
* Error handling utilities for MCP AI SDK provider.
|
||||
* Maps MCP errors to AI SDK compatible error types.
|
||||
*/
|
||||
|
||||
/**
|
||||
* MCP-specific error class
|
||||
*/
|
||||
export class MCPError extends Error {
|
||||
constructor(message, options = {}) {
|
||||
super(message);
|
||||
this.name = 'MCPError';
|
||||
this.code = options.code;
|
||||
this.cause = options.cause;
|
||||
this.mcpResponse = options.mcpResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session-related error
|
||||
*/
|
||||
export class MCPSessionError extends MCPError {
|
||||
constructor(message, options = {}) {
|
||||
super(message, options);
|
||||
this.name = 'MCPSessionError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sampling-related error
|
||||
*/
|
||||
export class MCPSamplingError extends MCPError {
|
||||
constructor(message, options = {}) {
|
||||
super(message, options);
|
||||
this.name = 'MCPSamplingError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map MCP errors to AI SDK compatible error types
|
||||
* @param {Error} error - Original error
|
||||
* @returns {Error} Mapped error
|
||||
*/
|
||||
export function mapMCPError(error) {
|
||||
// If already an MCP error, return as-is
|
||||
if (error instanceof MCPError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const message = error.message || 'Unknown MCP error';
|
||||
const originalError = error;
|
||||
|
||||
// Map common error patterns
|
||||
if (message.includes('session') || message.includes('connection')) {
|
||||
return new MCPSessionError(message, {
|
||||
cause: originalError,
|
||||
code: 'SESSION_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
if (message.includes('sampling') || message.includes('timeout')) {
|
||||
return new MCPSamplingError(message, {
|
||||
cause: originalError,
|
||||
code: 'SAMPLING_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
if (message.includes('capabilities') || message.includes('not supported')) {
|
||||
return new MCPSessionError(message, {
|
||||
cause: originalError,
|
||||
code: 'CAPABILITY_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
// Default to generic MCP error
|
||||
return new MCPError(message, {
|
||||
cause: originalError,
|
||||
code: 'UNKNOWN_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is retryable
|
||||
* @param {Error} error - Error to check
|
||||
* @returns {boolean} True if error might be retryable
|
||||
*/
|
||||
export function isRetryableError(error) {
|
||||
if (error instanceof MCPSamplingError && error.code === 'SAMPLING_ERROR') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error instanceof MCPSessionError && error.code === 'SESSION_ERROR') {
|
||||
// Session errors are generally not retryable
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common retryable patterns
|
||||
const message = error.message?.toLowerCase() || '';
|
||||
return (
|
||||
message.includes('timeout') ||
|
||||
message.includes('network') ||
|
||||
message.includes('temporary')
|
||||
);
|
||||
}
|
||||
47
mcp-server/src/custom-sdk/index.js
Normal file
47
mcp-server/src/custom-sdk/index.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* src/ai-providers/custom-sdk/mcp/index.js
|
||||
*
|
||||
* AI SDK factory function for MCP provider.
|
||||
* Creates MCP language model instances with session-based AI operations.
|
||||
*/
|
||||
|
||||
import { MCPLanguageModel } from './language-model.js';
|
||||
|
||||
/**
|
||||
* Create MCP provider factory function following AI SDK patterns
|
||||
* @param {object} options - Provider options
|
||||
* @param {object} options.session - MCP session object
|
||||
* @param {object} options.defaultSettings - Default settings for the provider
|
||||
* @returns {Function} Provider factory function
|
||||
*/
|
||||
export function createMCP(options = {}) {
|
||||
if (!options.session) {
|
||||
throw new Error('MCP provider requires session object');
|
||||
}
|
||||
|
||||
// Return the provider factory function that AI SDK expects
|
||||
const provider = function (modelId, settings = {}) {
|
||||
if (new.target) {
|
||||
throw new Error(
|
||||
'The MCP model function cannot be called with the new keyword.'
|
||||
);
|
||||
}
|
||||
|
||||
return new MCPLanguageModel({
|
||||
session: options.session,
|
||||
modelId: modelId || 'claude-3-5-sonnet-20241022',
|
||||
settings: {
|
||||
temperature: settings.temperature,
|
||||
maxTokens: settings.maxTokens,
|
||||
...options.defaultSettings,
|
||||
...settings
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add required methods for AI SDK compatibility
|
||||
provider.languageModel = (modelId, settings) => provider(modelId, settings);
|
||||
provider.chat = (modelId, settings) => provider(modelId, settings);
|
||||
|
||||
return provider;
|
||||
}
|
||||
109
mcp-server/src/custom-sdk/json-extractor.js
Normal file
109
mcp-server/src/custom-sdk/json-extractor.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @fileoverview Extract JSON from MCP response, handling markdown blocks and other formatting
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extract JSON from MCP AI response
|
||||
* @param {string} text - The text to extract JSON from
|
||||
* @returns {string} - The extracted JSON string
|
||||
*/
|
||||
export function extractJson(text) {
|
||||
// Remove markdown code blocks if present
|
||||
let jsonText = text.trim();
|
||||
|
||||
// Remove ```json blocks
|
||||
jsonText = jsonText.replace(/^```json\s*/gm, '');
|
||||
jsonText = jsonText.replace(/^```\s*/gm, '');
|
||||
jsonText = jsonText.replace(/```\s*$/gm, '');
|
||||
|
||||
// Remove common TypeScript/JavaScript patterns
|
||||
jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
|
||||
jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
|
||||
jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
|
||||
jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons
|
||||
|
||||
// Remove explanatory text before JSON (common with AI responses)
|
||||
jsonText = jsonText.replace(/^.*?(?=\{|\[)/s, '');
|
||||
|
||||
// Remove explanatory text after JSON
|
||||
const lines = jsonText.split('\n');
|
||||
let jsonEndIndex = -1;
|
||||
let braceCount = 0;
|
||||
let inString = false;
|
||||
let escapeNext = false;
|
||||
|
||||
// Find the end of the JSON by tracking braces
|
||||
for (let i = 0; i < jsonText.length; i++) {
|
||||
const char = jsonText[i];
|
||||
|
||||
if (escapeNext) {
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' && !escapeNext) {
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '{' || char === '[') {
|
||||
braceCount++;
|
||||
} else if (char === '}' || char === ']') {
|
||||
braceCount--;
|
||||
if (braceCount === 0) {
|
||||
jsonEndIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonEndIndex > -1) {
|
||||
jsonText = jsonText.substring(0, jsonEndIndex + 1);
|
||||
}
|
||||
|
||||
// Try to extract JSON object or array if previous method didn't work
|
||||
if (jsonEndIndex === -1) {
|
||||
const objectMatch = jsonText.match(/{[\s\S]*}/);
|
||||
const arrayMatch = jsonText.match(/\[[\s\S]*\]/);
|
||||
|
||||
if (objectMatch) {
|
||||
jsonText = objectMatch[0];
|
||||
} else if (arrayMatch) {
|
||||
jsonText = arrayMatch[0];
|
||||
}
|
||||
}
|
||||
|
||||
// First try to parse as valid JSON
|
||||
try {
|
||||
JSON.parse(jsonText);
|
||||
return jsonText;
|
||||
} catch {
|
||||
// If it's not valid JSON, it might be a JavaScript object literal
|
||||
// Try to convert it to valid JSON
|
||||
try {
|
||||
// This is a simple conversion that handles basic cases
|
||||
// Replace unquoted keys with quoted keys
|
||||
const converted = jsonText
|
||||
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
|
||||
// Replace single quotes with double quotes
|
||||
.replace(/'/g, '"')
|
||||
// Handle trailing commas
|
||||
.replace(/,\s*([}\]])/g, '$1');
|
||||
|
||||
// Validate the converted JSON
|
||||
JSON.parse(converted);
|
||||
return converted;
|
||||
} catch {
|
||||
// If all else fails, return the original text
|
||||
// The calling code will handle the error appropriately
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
230
mcp-server/src/custom-sdk/language-model.js
Normal file
230
mcp-server/src/custom-sdk/language-model.js
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* src/ai-providers/custom-sdk/mcp/language-model.js
|
||||
*
|
||||
* MCP Language Model implementation following AI SDK LanguageModelV1 interface.
|
||||
* Uses MCP session.requestSampling() for AI operations.
|
||||
*/
|
||||
|
||||
import {
|
||||
convertToMCPFormat,
|
||||
convertFromMCPFormat
|
||||
} from './message-converter.js';
|
||||
import { MCPError, mapMCPError } from './errors.js';
|
||||
import { extractJson } from './json-extractor.js';
|
||||
import {
|
||||
convertSchemaToInstructions,
|
||||
enhancePromptForJSON
|
||||
} from './schema-converter.js';
|
||||
|
||||
/**
|
||||
* MCP Language Model implementing AI SDK LanguageModelV1 interface
|
||||
*/
|
||||
export class MCPLanguageModel {
|
||||
specificationVersion = 'v1';
|
||||
defaultObjectGenerationMode = 'json';
|
||||
supportsImageUrls = false;
|
||||
supportsStructuredOutputs = true;
|
||||
|
||||
constructor(options) {
|
||||
this.session = options.session; // MCP session object
|
||||
this.modelId = options.modelId;
|
||||
this.settings = options.settings || {};
|
||||
this.provider = 'mcp-ai-sdk';
|
||||
this.maxTokens = this.settings.maxTokens;
|
||||
this.temperature = this.settings.temperature;
|
||||
|
||||
this.validateSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the MCP session has required capabilities
|
||||
*/
|
||||
validateSession() {
|
||||
if (!this.session?.clientCapabilities?.sampling) {
|
||||
throw new MCPError('MCP session must have client sampling capabilities');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate text using MCP session sampling
|
||||
* @param {object} options - Generation options
|
||||
* @param {Array} options.prompt - AI SDK prompt format
|
||||
* @param {AbortSignal} options.abortSignal - Abort signal
|
||||
* @returns {Promise<object>} Generation result in AI SDK format
|
||||
*/
|
||||
async doGenerate(options) {
|
||||
try {
|
||||
// Convert AI SDK prompt to MCP format
|
||||
const { messages, systemPrompt } = convertToMCPFormat(options.prompt);
|
||||
|
||||
// Use MCP session.requestSampling (same as MCPRemoteProvider)
|
||||
const response = await this.session.requestSampling(
|
||||
{
|
||||
messages,
|
||||
systemPrompt,
|
||||
temperature: this.settings.temperature,
|
||||
maxTokens: this.settings.maxTokens,
|
||||
includeContext: 'thisServer'
|
||||
},
|
||||
{
|
||||
// signal: options.abortSignal,
|
||||
timeout: 240000 // 4 minutes timeout
|
||||
}
|
||||
);
|
||||
|
||||
// Convert MCP response back to AI SDK format
|
||||
const result = convertFromMCPFormat(response);
|
||||
|
||||
return {
|
||||
text: result.text,
|
||||
finishReason: result.finishReason || 'stop',
|
||||
usage: {
|
||||
promptTokens: result.usage?.inputTokens || 0,
|
||||
completionTokens: result.usage?.outputTokens || 0,
|
||||
totalTokens:
|
||||
(result.usage?.inputTokens || 0) + (result.usage?.outputTokens || 0)
|
||||
},
|
||||
rawResponse: response,
|
||||
warnings: result.warnings
|
||||
};
|
||||
} catch (error) {
|
||||
throw mapMCPError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate structured object using MCP session sampling
|
||||
* @param {object} options - Generation options
|
||||
* @param {Array} options.prompt - AI SDK prompt format
|
||||
* @param {import('zod').ZodSchema} options.schema - Zod schema for validation
|
||||
* @param {string} [options.mode='json'] - Generation mode ('json' or 'tool')
|
||||
* @param {AbortSignal} options.abortSignal - Abort signal
|
||||
* @returns {Promise<object>} Generation result with structured object
|
||||
*/
|
||||
async doGenerateObject(options) {
|
||||
try {
|
||||
const { schema, mode = 'json', ...restOptions } = options;
|
||||
|
||||
if (!schema) {
|
||||
throw new MCPError('Schema is required for object generation');
|
||||
}
|
||||
|
||||
// Convert schema to JSON instructions
|
||||
const objectName = restOptions.objectName || 'generated_object';
|
||||
const jsonInstructions = convertSchemaToInstructions(schema, objectName);
|
||||
|
||||
// Enhance prompt with JSON generation instructions
|
||||
const enhancedPrompt = enhancePromptForJSON(
|
||||
options.prompt,
|
||||
jsonInstructions
|
||||
);
|
||||
|
||||
// Convert enhanced prompt to MCP format
|
||||
const { messages, systemPrompt } = convertToMCPFormat(enhancedPrompt);
|
||||
|
||||
// Use MCP session.requestSampling with enhanced prompt
|
||||
const response = await this.session.requestSampling(
|
||||
{
|
||||
messages,
|
||||
systemPrompt,
|
||||
temperature: this.settings.temperature,
|
||||
maxTokens: this.settings.maxTokens,
|
||||
includeContext: 'thisServer'
|
||||
},
|
||||
{
|
||||
timeout: 240000 // 4 minutes timeout
|
||||
}
|
||||
);
|
||||
|
||||
// Convert MCP response back to AI SDK format
|
||||
const result = convertFromMCPFormat(response);
|
||||
|
||||
// Extract JSON from the response text
|
||||
const jsonText = extractJson(result.text);
|
||||
|
||||
// Parse and validate JSON
|
||||
let parsedObject;
|
||||
try {
|
||||
parsedObject = JSON.parse(jsonText);
|
||||
} catch (parseError) {
|
||||
throw new MCPError(
|
||||
`Failed to parse JSON response: ${parseError.message}. Response: ${result.text.substring(0, 200)}...`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate against schema
|
||||
try {
|
||||
const validatedObject = schema.parse(parsedObject);
|
||||
|
||||
return {
|
||||
object: validatedObject,
|
||||
finishReason: result.finishReason || 'stop',
|
||||
usage: {
|
||||
promptTokens: result.usage?.inputTokens || 0,
|
||||
completionTokens: result.usage?.outputTokens || 0,
|
||||
totalTokens:
|
||||
(result.usage?.inputTokens || 0) +
|
||||
(result.usage?.outputTokens || 0)
|
||||
},
|
||||
rawResponse: response,
|
||||
warnings: result.warnings
|
||||
};
|
||||
} catch (validationError) {
|
||||
throw new MCPError(
|
||||
`Generated object does not match schema: ${validationError.message}. Generated: ${JSON.stringify(parsedObject, null, 2)}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw mapMCPError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream text generation using MCP session sampling
|
||||
* Note: MCP may not support native streaming, so this may simulate streaming
|
||||
* @param {object} options - Generation options
|
||||
* @returns {AsyncIterable} Stream of generation chunks
|
||||
*/
|
||||
async doStream(options) {
|
||||
try {
|
||||
// For now, simulate streaming by chunking the complete response
|
||||
// TODO: Implement native streaming if MCP supports it
|
||||
const result = await this.doGenerate(options);
|
||||
|
||||
// Create async generator that yields chunks
|
||||
return this.simulateStreaming(result);
|
||||
} catch (error) {
|
||||
throw mapMCPError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate streaming by chunking a complete response
|
||||
* @param {object} result - Complete generation result
|
||||
* @returns {AsyncIterable} Simulated stream chunks
|
||||
*/
|
||||
async *simulateStreaming(result) {
|
||||
const text = result.text;
|
||||
const chunkSize = Math.max(1, Math.floor(text.length / 10)); // 10 chunks
|
||||
|
||||
for (let i = 0; i < text.length; i += chunkSize) {
|
||||
const chunk = text.slice(i, i + chunkSize);
|
||||
const isLast = i + chunkSize >= text.length;
|
||||
|
||||
yield {
|
||||
type: 'text-delta',
|
||||
textDelta: chunk
|
||||
};
|
||||
|
||||
// Small delay to simulate streaming
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
// Final chunk with finish reason and usage
|
||||
yield {
|
||||
type: 'finish',
|
||||
finishReason: result.finishReason,
|
||||
usage: result.usage
|
||||
};
|
||||
}
|
||||
}
|
||||
116
mcp-server/src/custom-sdk/message-converter.js
Normal file
116
mcp-server/src/custom-sdk/message-converter.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* src/ai-providers/custom-sdk/mcp/message-converter.js
|
||||
*
|
||||
* Message conversion utilities for converting between AI SDK prompt format
|
||||
* and MCP sampling format.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert AI SDK prompt format to MCP sampling format
|
||||
* @param {Array} prompt - AI SDK prompt array
|
||||
* @returns {object} MCP format with messages and systemPrompt
|
||||
*/
|
||||
export function convertToMCPFormat(prompt) {
|
||||
const messages = [];
|
||||
let systemPrompt = '';
|
||||
|
||||
for (const message of prompt) {
|
||||
if (message.role === 'system') {
|
||||
// Extract system prompt
|
||||
systemPrompt = extractTextContent(message.content);
|
||||
} else if (message.role === 'user' || message.role === 'assistant') {
|
||||
// Convert user/assistant messages
|
||||
messages.push({
|
||||
role: message.role,
|
||||
content: {
|
||||
type: 'text',
|
||||
text: extractTextContent(message.content)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
systemPrompt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MCP response format to AI SDK format
|
||||
* @param {object} response - MCP sampling response
|
||||
* @returns {object} AI SDK compatible result
|
||||
*/
|
||||
export function convertFromMCPFormat(response) {
|
||||
// Handle different possible response formats
|
||||
let text = '';
|
||||
let usage = null;
|
||||
let finishReason = 'stop';
|
||||
let warnings = [];
|
||||
|
||||
if (typeof response === 'string') {
|
||||
text = response;
|
||||
} else if (response.content) {
|
||||
text = extractTextContent(response.content);
|
||||
usage = response.usage;
|
||||
finishReason = response.finishReason || 'stop';
|
||||
} else if (response.text) {
|
||||
text = response.text;
|
||||
usage = response.usage;
|
||||
finishReason = response.finishReason || 'stop';
|
||||
} else {
|
||||
// Fallback: try to extract text from response
|
||||
text = JSON.stringify(response);
|
||||
warnings.push('Unexpected MCP response format, used JSON fallback');
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
usage,
|
||||
finishReason,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from various content formats
|
||||
* @param {string|Array|object} content - Content in various formats
|
||||
* @returns {string} Extracted text
|
||||
*/
|
||||
function extractTextContent(content) {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
// Handle array of content parts
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === 'string') {
|
||||
return part;
|
||||
}
|
||||
if (part.type === 'text' && part.text) {
|
||||
return part.text;
|
||||
}
|
||||
if (part.text) {
|
||||
return part.text;
|
||||
}
|
||||
// Skip non-text content (images, etc.)
|
||||
return '';
|
||||
})
|
||||
.filter((text) => text.length > 0)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
if (content && typeof content === 'object') {
|
||||
if (content.type === 'text' && content.text) {
|
||||
return content.text;
|
||||
}
|
||||
if (content.text) {
|
||||
return content.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return String(content || '');
|
||||
}
|
||||
150
mcp-server/src/custom-sdk/schema-converter.js
Normal file
150
mcp-server/src/custom-sdk/schema-converter.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @fileoverview Schema conversion utilities for MCP AI SDK provider
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert Zod schema to human-readable JSON instructions
|
||||
* @param {import('zod').ZodSchema} schema - Zod schema object
|
||||
* @param {string} [objectName='result'] - Name of the object being generated
|
||||
* @returns {string} Instructions for JSON generation
|
||||
*/
|
||||
export function convertSchemaToInstructions(schema, objectName = 'result') {
|
||||
try {
|
||||
// Generate example structure from schema
|
||||
const exampleStructure = generateExampleFromSchema(schema);
|
||||
|
||||
return `
|
||||
CRITICAL JSON GENERATION INSTRUCTIONS:
|
||||
|
||||
You must respond with ONLY valid JSON that matches this exact structure for "${objectName}":
|
||||
|
||||
${JSON.stringify(exampleStructure, null, 2)}
|
||||
|
||||
STRICT REQUIREMENTS:
|
||||
1. Response must start with { and end with }
|
||||
2. Use double quotes for all strings and property names
|
||||
3. Do not include any text before or after the JSON
|
||||
4. Do not wrap in markdown code blocks
|
||||
5. Do not include explanations or comments
|
||||
6. Follow the exact property names and types shown above
|
||||
7. All required fields must be present
|
||||
|
||||
Begin your response immediately with the opening brace {`;
|
||||
} catch (error) {
|
||||
// Fallback to basic JSON instructions if schema parsing fails
|
||||
return `
|
||||
CRITICAL JSON GENERATION INSTRUCTIONS:
|
||||
|
||||
You must respond with ONLY valid JSON for "${objectName}".
|
||||
|
||||
STRICT REQUIREMENTS:
|
||||
1. Response must start with { and end with }
|
||||
2. Use double quotes for all strings and property names
|
||||
3. Do not include any text before or after the JSON
|
||||
4. Do not wrap in markdown code blocks
|
||||
5. Do not include explanations or comments
|
||||
|
||||
Begin your response immediately with the opening brace {`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate example structure from Zod schema
|
||||
* @param {import('zod').ZodSchema} schema - Zod schema
|
||||
* @returns {any} Example object matching the schema
|
||||
*/
|
||||
function generateExampleFromSchema(schema) {
|
||||
// This is a simplified schema-to-example converter
|
||||
// For production, you might want to use a more sophisticated library
|
||||
|
||||
if (!schema || typeof schema._def === 'undefined') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const def = schema._def;
|
||||
|
||||
switch (def.typeName) {
|
||||
case 'ZodObject':
|
||||
const result = {};
|
||||
const shape = def.shape();
|
||||
|
||||
for (const [key, fieldSchema] of Object.entries(shape)) {
|
||||
result[key] = generateExampleFromSchema(fieldSchema);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
case 'ZodString':
|
||||
return 'string';
|
||||
|
||||
case 'ZodNumber':
|
||||
return 0;
|
||||
|
||||
case 'ZodBoolean':
|
||||
return false;
|
||||
|
||||
case 'ZodArray':
|
||||
const elementExample = generateExampleFromSchema(def.type);
|
||||
return [elementExample];
|
||||
|
||||
case 'ZodOptional':
|
||||
return generateExampleFromSchema(def.innerType);
|
||||
|
||||
case 'ZodNullable':
|
||||
return generateExampleFromSchema(def.innerType);
|
||||
|
||||
case 'ZodEnum':
|
||||
return def.values[0] || 'enum_value';
|
||||
|
||||
case 'ZodLiteral':
|
||||
return def.value;
|
||||
|
||||
case 'ZodUnion':
|
||||
// Use the first option from the union
|
||||
if (def.options && def.options.length > 0) {
|
||||
return generateExampleFromSchema(def.options[0]);
|
||||
}
|
||||
return 'union_value';
|
||||
|
||||
case 'ZodRecord':
|
||||
return {
|
||||
key: generateExampleFromSchema(def.valueType)
|
||||
};
|
||||
|
||||
default:
|
||||
// For unknown types, return a placeholder
|
||||
return `<${def.typeName || 'unknown'}>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance prompt with JSON generation instructions
|
||||
* @param {Array} prompt - AI SDK prompt array
|
||||
* @param {string} jsonInstructions - JSON generation instructions
|
||||
* @returns {Array} Enhanced prompt array
|
||||
*/
|
||||
export function enhancePromptForJSON(prompt, jsonInstructions) {
|
||||
const enhancedPrompt = [...prompt];
|
||||
|
||||
// Find system message or create one
|
||||
let systemMessageIndex = enhancedPrompt.findIndex(
|
||||
(msg) => msg.role === 'system'
|
||||
);
|
||||
|
||||
if (systemMessageIndex >= 0) {
|
||||
// Append to existing system message
|
||||
const currentContent = enhancedPrompt[systemMessageIndex].content;
|
||||
enhancedPrompt[systemMessageIndex] = {
|
||||
...enhancedPrompt[systemMessageIndex],
|
||||
content: currentContent + '\n\n' + jsonInstructions
|
||||
};
|
||||
} else {
|
||||
// Add new system message at the beginning
|
||||
enhancedPrompt.unshift({
|
||||
role: 'system',
|
||||
content: jsonInstructions
|
||||
});
|
||||
}
|
||||
|
||||
return enhancedPrompt;
|
||||
}
|
||||
Reference in New Issue
Block a user