mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat: fix context7 mcp tool calling mode
- todo, upgrade to ai sdk v6 (openrouter not upgraded to ai sdk v6 yet, waiting on that)
This commit is contained in:
5225
package-lock.json
generated
5225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -58,30 +58,31 @@
|
|||||||
"author": "Eyal Toledano",
|
"author": "Eyal Toledano",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.23",
|
"@ai-sdk/amazon-bedrock": "^4.0.9",
|
||||||
"@ai-sdk/anthropic": "^2.0.18",
|
"@ai-sdk/anthropic": "^3.0.7",
|
||||||
"@ai-sdk/azure": "^2.0.89",
|
"@ai-sdk/azure": "^3.0.5",
|
||||||
"@ai-sdk/google": "^2.0.16",
|
"@ai-sdk/google": "^3.0.4",
|
||||||
"@ai-sdk/google-vertex": "^3.0.86",
|
"@ai-sdk/google-vertex": "^4.0.7",
|
||||||
"@ai-sdk/groq": "^2.0.21",
|
"@ai-sdk/groq": "^3.0.4",
|
||||||
"@ai-sdk/mistral": "^2.0.16",
|
"@ai-sdk/mcp": "^1.0.5",
|
||||||
"@ai-sdk/openai": "^2.0.34",
|
"@ai-sdk/mistral": "^3.0.5",
|
||||||
"@ai-sdk/openai-compatible": "^1.0.25",
|
"@ai-sdk/openai": "^3.0.5",
|
||||||
"@ai-sdk/perplexity": "^2.0.10",
|
"@ai-sdk/openai-compatible": "^2.0.4",
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/perplexity": "^3.0.4",
|
||||||
"@ai-sdk/provider-utils": "^3.0.10",
|
"@ai-sdk/provider": "^3.0.2",
|
||||||
"@ai-sdk/xai": "^2.0.22",
|
"@ai-sdk/provider-utils": "^4.0.4",
|
||||||
"@aws-sdk/credential-providers": "^3.895.0",
|
"@ai-sdk/xai": "^3.0.10",
|
||||||
"@inquirer/search": "^3.0.15",
|
"@aws-sdk/credential-providers": "^3.962.0",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.0",
|
"@inquirer/search": "^3.2.2",
|
||||||
"@sentry/node": "^10.27.0",
|
"@openrouter/ai-sdk-provider": "^1.5.4",
|
||||||
|
"@sentry/node": "^10.32.1",
|
||||||
"@streamparser/json": "^0.0.22",
|
"@streamparser/json": "^0.0.22",
|
||||||
"@supabase/supabase-js": "^2.57.4",
|
"@supabase/supabase-js": "^2.57.4",
|
||||||
"@types/turndown": "^5.0.6",
|
"@types/turndown": "^5.0.6",
|
||||||
"ai": "^5.0.51",
|
"ai": "^6.0.13",
|
||||||
"ai-sdk-provider-claude-code": "^2.2.4",
|
"ai-sdk-provider-claude-code": "^3.1.0",
|
||||||
"ai-sdk-provider-codex-cli": "^0.7.0",
|
"ai-sdk-provider-codex-cli": "^1.0.4",
|
||||||
"ai-sdk-provider-gemini-cli": "^1.4.0",
|
"ai-sdk-provider-gemini-cli": "^2.0.1",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
"boxen": "^8.0.1",
|
"boxen": "^8.0.1",
|
||||||
|
|||||||
@@ -15,14 +15,14 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/provider": "^3.0.2",
|
||||||
"@ai-sdk/provider-utils": "^3.0.10",
|
"@ai-sdk/provider-utils": "^4.0.4",
|
||||||
"jsonc-parser": "^3.3.1"
|
"jsonc-parser": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.18.6",
|
"@types/node": "^22.18.6",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^4.0.10"
|
"vitest": "^4.0.16"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ describe('createGrokCli', () => {
|
|||||||
expect(typeof provider).toBe('function');
|
expect(typeof provider).toBe('function');
|
||||||
expect(typeof provider.languageModel).toBe('function');
|
expect(typeof provider.languageModel).toBe('function');
|
||||||
expect(typeof provider.chat).toBe('function');
|
expect(typeof provider.chat).toBe('function');
|
||||||
expect(typeof provider.textEmbeddingModel).toBe('function');
|
expect(typeof provider.embeddingModel).toBe('function');
|
||||||
expect(typeof provider.imageModel).toBe('function');
|
expect(typeof provider.imageModel).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ describe('createGrokCli', () => {
|
|||||||
it('should throw NoSuchModelError for textEmbeddingModel', () => {
|
it('should throw NoSuchModelError for textEmbeddingModel', () => {
|
||||||
const provider = createGrokCli();
|
const provider = createGrokCli();
|
||||||
expect(() => {
|
expect(() => {
|
||||||
provider.textEmbeddingModel('test-model');
|
provider.embeddingModel('test-model');
|
||||||
}).toThrow(NoSuchModelError);
|
}).toThrow(NoSuchModelError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function createGrokCli(
|
|||||||
provider.chat = createModel; // Alias for languageModel
|
provider.chat = createModel; // Alias for languageModel
|
||||||
|
|
||||||
// Add textEmbeddingModel method that throws NoSuchModelError
|
// Add textEmbeddingModel method that throws NoSuchModelError
|
||||||
provider.textEmbeddingModel = (modelId: string) => {
|
provider.embeddingModel = (modelId: string) => {
|
||||||
throw new NoSuchModelError({
|
throw new NoSuchModelError({
|
||||||
modelId,
|
modelId,
|
||||||
modelType: 'textEmbeddingModel'
|
modelType: 'textEmbeddingModel'
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export async function createContext7Tools(
|
|||||||
): Promise<Context7ToolsResult> {
|
): Promise<Context7ToolsResult> {
|
||||||
const apiKey = getApiKey(options?.config);
|
const apiKey = getApiKey(options?.config);
|
||||||
|
|
||||||
|
console.log('[Context7] Initializing MCP client...');
|
||||||
|
|
||||||
const transport = new Experimental_StdioMCPTransport({
|
const transport = new Experimental_StdioMCPTransport({
|
||||||
command: 'npx',
|
command: 'npx',
|
||||||
args: ['-y', '@upstash/context7-mcp', '--api-key', apiKey]
|
args: ['-y', '@upstash/context7-mcp', '--api-key', apiKey]
|
||||||
@@ -69,14 +71,23 @@ export async function createContext7Tools(
|
|||||||
transport
|
transport
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[Context7] MCP client connected successfully');
|
||||||
|
|
||||||
options?.onReady?.();
|
options?.onReady?.();
|
||||||
|
|
||||||
const tools = await client.tools();
|
const tools = await client.tools();
|
||||||
|
const toolNames = Object.keys(tools);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[Context7] Tools available: ${toolNames.length > 0 ? toolNames.join(', ') : 'none'}`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools,
|
tools,
|
||||||
close: async () => {
|
close: async () => {
|
||||||
|
console.log('[Context7] Closing MCP client connection...');
|
||||||
await client.close();
|
await client.close();
|
||||||
|
console.log('[Context7] MCP client disconnected');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -87,6 +98,7 @@ export async function createContext7Tools(
|
|||||||
export function isContext7Available(config?: Context7Config): boolean {
|
export function isContext7Available(config?: Context7Config): boolean {
|
||||||
try {
|
try {
|
||||||
getApiKey(config);
|
getApiKey(config);
|
||||||
|
console.log('[Context7] API key detected - Context7 tools will be enabled');
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
// Vercel AI SDK functions are NOT called directly anymore.
|
// Vercel AI SDK functions are NOT called directly anymore.
|
||||||
// import { generateText, streamText, generateObject } from 'ai';
|
// import { generateText, streamText, generateObject } from 'ai';
|
||||||
|
|
||||||
|
// --- MCP Tools Integration ---
|
||||||
|
import { createMCPTools, isContext7Available } from '@tm/ai-tools';
|
||||||
|
|
||||||
// --- Core Dependencies ---
|
// --- Core Dependencies ---
|
||||||
import {
|
import {
|
||||||
MODEL_MAP,
|
MODEL_MAP,
|
||||||
@@ -59,6 +62,84 @@ import {
|
|||||||
// Import the provider registry
|
// Import the provider registry
|
||||||
import ProviderRegistry from '../../src/provider-registry/index.js';
|
import ProviderRegistry from '../../src/provider-registry/index.js';
|
||||||
|
|
||||||
|
// --- MCP Tools Cache ---
|
||||||
|
let _mcpToolsCache = null;
|
||||||
|
let _mcpToolsPromise = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create MCP tools (singleton pattern for connection reuse)
|
||||||
|
* @returns {Promise<{tools: Record<string, unknown>, close: () => Promise<void>, enabledSources: string[]}>}
|
||||||
|
*/
|
||||||
|
async function _getMCPTools() {
|
||||||
|
// Return cached tools if available
|
||||||
|
if (_mcpToolsCache) {
|
||||||
|
return _mcpToolsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return existing promise if tools are being created
|
||||||
|
if (_mcpToolsPromise) {
|
||||||
|
return _mcpToolsPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any MCP tools are available
|
||||||
|
if (!isContext7Available()) {
|
||||||
|
return { tools: {}, close: async () => {}, enabledSources: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tools (this spawns MCP server subprocess)
|
||||||
|
_mcpToolsPromise = createMCPTools()
|
||||||
|
.then((result) => {
|
||||||
|
_mcpToolsCache = result;
|
||||||
|
_mcpToolsPromise = null;
|
||||||
|
|
||||||
|
if (result.enabledSources.length > 0) {
|
||||||
|
log('info', `MCP tools enabled: ${result.enabledSources.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
_mcpToolsPromise = null;
|
||||||
|
log('warn', `Failed to initialize MCP tools: ${error.message}`);
|
||||||
|
return { tools: {}, close: async () => {}, enabledSources: [] };
|
||||||
|
});
|
||||||
|
|
||||||
|
return _mcpToolsPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close MCP tools connections (call on process exit)
|
||||||
|
*/
|
||||||
|
async function closeMCPTools() {
|
||||||
|
if (_mcpToolsCache) {
|
||||||
|
try {
|
||||||
|
await _mcpToolsCache.close();
|
||||||
|
log('debug', 'MCP tools connections closed');
|
||||||
|
} catch (error) {
|
||||||
|
log('warn', `Error closing MCP tools: ${error.message}`);
|
||||||
|
}
|
||||||
|
_mcpToolsCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register cleanup on process exit
|
||||||
|
process.on('exit', () => {
|
||||||
|
if (_mcpToolsCache) {
|
||||||
|
// Synchronous cleanup attempt - close() is async so may not complete
|
||||||
|
_mcpToolsCache.close().catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
await closeMCPTools();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await closeMCPTools();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
// Create provider instances
|
// Create provider instances
|
||||||
const PROVIDERS = {
|
const PROVIDERS = {
|
||||||
anthropic: new AnthropicAIProvider(),
|
anthropic: new AnthropicAIProvider(),
|
||||||
@@ -544,6 +625,17 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
let lastCleanErrorMessage =
|
let lastCleanErrorMessage =
|
||||||
'AI service call failed for all configured roles.';
|
'AI service call failed for all configured roles.';
|
||||||
|
|
||||||
|
// Get MCP tools (cached singleton - won't recreate on each call)
|
||||||
|
const mcpTools = await _getMCPTools();
|
||||||
|
const hasTools = Object.keys(mcpTools.tools).length > 0;
|
||||||
|
|
||||||
|
if (hasTools && getDebugFlag()) {
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`MCP tools available for AI call: ${mcpTools.enabledSources.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const currentRole of sequence) {
|
for (const currentRole of sequence) {
|
||||||
let providerName;
|
let providerName;
|
||||||
let modelId;
|
let modelId;
|
||||||
@@ -676,6 +768,7 @@ async function _unifiedServiceRunner(serviceType, params) {
|
|||||||
...(baseURL && { baseURL }),
|
...(baseURL && { baseURL }),
|
||||||
...((serviceType === 'generateObject' ||
|
...((serviceType === 'generateObject' ||
|
||||||
serviceType === 'streamObject') && { schema, objectName }),
|
serviceType === 'streamObject') && { schema, objectName }),
|
||||||
|
...(hasTools && { tools: mcpTools.tools }),
|
||||||
...(commandName && { commandName }), // Pass commandName for Sentry telemetry functionId
|
...(commandName && { commandName }), // Pass commandName for Sentry telemetry functionId
|
||||||
...(outputType && { outputType }), // Pass outputType for Sentry telemetry metadata
|
...(outputType && { outputType }), // Pass outputType for Sentry telemetry metadata
|
||||||
...(projectRoot && { projectRoot }), // Pass projectRoot for Sentry telemetry hashing
|
...(projectRoot && { projectRoot }), // Pass projectRoot for Sentry telemetry hashing
|
||||||
@@ -951,5 +1044,6 @@ export {
|
|||||||
streamTextService,
|
streamTextService,
|
||||||
streamObjectService,
|
streamObjectService,
|
||||||
generateObjectService,
|
generateObjectService,
|
||||||
logAiUsage
|
logAiUsage,
|
||||||
|
closeMCPTools
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1048,24 +1048,8 @@ function getAvailableModels() {
|
|||||||
const sweScore = modelObj.swe_score;
|
const sweScore = modelObj.swe_score;
|
||||||
const cost = modelObj.cost_per_1m_tokens;
|
const cost = modelObj.cost_per_1m_tokens;
|
||||||
const allowedRoles = modelObj.allowed_roles || ['main', 'fallback'];
|
const allowedRoles = modelObj.allowed_roles || ['main', 'fallback'];
|
||||||
|
// Use explicit name from model definition, otherwise use ID as-is
|
||||||
// Use name from JSON if available, otherwise generate from ID
|
const name = modelObj.name || modelId;
|
||||||
let name = modelObj.name;
|
|
||||||
if (!name) {
|
|
||||||
const nameParts = modelId
|
|
||||||
.split('-')
|
|
||||||
.map((p) => p.charAt(0).toUpperCase() + p.slice(1));
|
|
||||||
name = nameParts.join(' ');
|
|
||||||
// Handle specific known names better if needed
|
|
||||||
if (modelId === 'claude-3.5-sonnet-20240620')
|
|
||||||
name = 'Claude 3.5 Sonnet';
|
|
||||||
if (modelId === 'claude-3-7-sonnet-20250219')
|
|
||||||
name = 'Claude 3.7 Sonnet';
|
|
||||||
if (modelId === 'gpt-4o') name = 'GPT-4o';
|
|
||||||
if (modelId === 'gpt-4-turbo') name = 'GPT-4 Turbo';
|
|
||||||
if (modelId === 'sonar-pro') name = 'Perplexity Sonar Pro';
|
|
||||||
if (modelId === 'sonar-mini') name = 'Perplexity Sonar Mini';
|
|
||||||
}
|
|
||||||
|
|
||||||
available.push({
|
available.push({
|
||||||
id: modelId,
|
id: modelId,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// todo: migrate to new ai sdk v6 correctly, use streamText and generateText instead of streamObject and generateObject with the right parameters
|
||||||
import {
|
import {
|
||||||
JSONParseError,
|
JSONParseError,
|
||||||
NoObjectGeneratedError,
|
NoObjectGeneratedError,
|
||||||
@@ -196,6 +197,7 @@ export class BaseAIProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates text using the provider's model
|
* Generates text using the provider's model
|
||||||
|
* Supports tools parameter for MCP tool integration
|
||||||
*/
|
*/
|
||||||
async generateText(params) {
|
async generateText(params) {
|
||||||
try {
|
try {
|
||||||
@@ -233,6 +235,7 @@ export class BaseAIProvider {
|
|||||||
...(this.supportsTemperature && params.temperature !== undefined
|
...(this.supportsTemperature && params.temperature !== undefined
|
||||||
? { temperature: params.temperature }
|
? { temperature: params.temperature }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(params.tools ? { tools: params.tools } : {}),
|
||||||
...(telemetryConfig && { experimental_telemetry: telemetryConfig })
|
...(telemetryConfig && { experimental_telemetry: telemetryConfig })
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,6 +266,7 @@ export class BaseAIProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams text using the provider's model
|
* Streams text using the provider's model
|
||||||
|
* Supports tools parameter for MCP tool integration
|
||||||
*/
|
*/
|
||||||
async streamText(params) {
|
async streamText(params) {
|
||||||
try {
|
try {
|
||||||
@@ -297,6 +301,7 @@ export class BaseAIProvider {
|
|||||||
...(this.supportsTemperature && params.temperature !== undefined
|
...(this.supportsTemperature && params.temperature !== undefined
|
||||||
? { temperature: params.temperature }
|
? { temperature: params.temperature }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(params.tools ? { tools: params.tools } : {}),
|
||||||
...(telemetryConfig && { experimental_telemetry: telemetryConfig }),
|
...(telemetryConfig && { experimental_telemetry: telemetryConfig }),
|
||||||
...(params.experimental_transform && {
|
...(params.experimental_transform && {
|
||||||
experimental_transform: params.experimental_transform
|
experimental_transform: params.experimental_transform
|
||||||
@@ -316,6 +321,7 @@ export class BaseAIProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Streams a structured object using the provider's model
|
* Streams a structured object using the provider's model
|
||||||
|
* Note: AI SDK v5 streamObject doesn't support tools - use streamText for tool support
|
||||||
*/
|
*/
|
||||||
async streamObject(params) {
|
async streamObject(params) {
|
||||||
try {
|
try {
|
||||||
@@ -350,6 +356,7 @@ export class BaseAIProvider {
|
|||||||
|
|
||||||
const telemetryConfig = getAITelemetryConfig(functionId, metadata);
|
const telemetryConfig = getAITelemetryConfig(functionId, metadata);
|
||||||
|
|
||||||
|
// AI SDK v5: Use streamObject directly (no tool support)
|
||||||
const result = await streamObject({
|
const result = await streamObject({
|
||||||
model: client(params.modelId),
|
model: client(params.modelId),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
@@ -368,7 +375,6 @@ export class BaseAIProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Return the stream result directly
|
// Return the stream result directly
|
||||||
// The stream result contains partialObjectStream and other properties
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError('object streaming', error);
|
this.handleError('object streaming', error);
|
||||||
@@ -377,6 +383,7 @@ export class BaseAIProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a structured object using the provider's model
|
* Generates a structured object using the provider's model
|
||||||
|
* Note: AI SDK v5 generateObject doesn't support tools - use generateText for tool support
|
||||||
*/
|
*/
|
||||||
async generateObject(params) {
|
async generateObject(params) {
|
||||||
try {
|
try {
|
||||||
@@ -414,6 +421,7 @@ export class BaseAIProvider {
|
|||||||
|
|
||||||
const telemetryConfig = getAITelemetryConfig(functionId, metadata);
|
const telemetryConfig = getAITelemetryConfig(functionId, metadata);
|
||||||
|
|
||||||
|
// AI SDK v5: Use generateObject directly (no tool support)
|
||||||
const result = await generateObject({
|
const result = await generateObject({
|
||||||
model: client(params.modelId),
|
model: client(params.modelId),
|
||||||
messages: params.messages,
|
messages: params.messages,
|
||||||
|
|||||||
Reference in New Issue
Block a user