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:
5
.changeset/some-lies-grin.md
Normal file
5
.changeset/some-lies-grin.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add support for MCP Sampling as AI provider, requires no API key, uses the client LLM provider
|
||||
@@ -11,10 +11,11 @@ Welcome to the Task Master documentation. Use the links below to navigate to the
|
||||
|
||||
- [Command Reference](command-reference.md) - Complete list of all available commands (including research and multi-task viewing)
|
||||
- [Task Structure](task-structure.md) - Understanding the task format and features
|
||||
- [Available Models](models.md) - Complete list of supported AI models and providers
|
||||
|
||||
## Examples & Licensing
|
||||
|
||||
- [Example Interactions](examples.md) - Common Cursor AI interaction examples
|
||||
- [Example Interactions](examples.md) - Common Cursor AI interaction examples
|
||||
- [Licensing Information](licensing.md) - Detailed information about the license
|
||||
|
||||
## Need More Help?
|
||||
|
||||
@@ -4,7 +4,30 @@ Taskmaster uses two primary methods for configuration:
|
||||
|
||||
1. **`.taskmaster/config.json` File (Recommended - New Structure)**
|
||||
|
||||
- This JSON file stores most configuration settings, including AI model selections, parameters, logging levels, and project defaults.
|
||||
- This JSON file stores most configuration settings, including A5. **Usage Requirements**:
|
||||
8. **Troubleshooting**:
|
||||
- "MCP provider requires session context" → Ensure running in MCP environment
|
||||
- See the [MCP Provider Guide](./mcp-provider-guide.md) for detailed troubleshootingust be running in an MCP context (session must be available)
|
||||
- Session must provide `clientCapabilities.sampling` capability
|
||||
|
||||
6. **Best Practices**:
|
||||
- Always configure a non-MCP fallback provider
|
||||
- Use `mcp` for main/research roles when in MCP environments
|
||||
- Test sampling capability before production use
|
||||
|
||||
7. **Setup Commands**:
|
||||
```bash
|
||||
# Set MCP provider for main role
|
||||
task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022
|
||||
|
||||
# Set MCP provider for research role
|
||||
task-master models set-research --provider mcp --model claude-3-opus-20240229
|
||||
|
||||
# Verify configuration
|
||||
task-master models list
|
||||
```
|
||||
|
||||
8. **Troubleshooting**:lections, parameters, logging levels, and project defaults.
|
||||
- **Location:** This file is created in the `.taskmaster/` directory when you run the `task-master models --setup` interactive setup or initialize a new project with `task-master init`.
|
||||
- **Migration:** Existing projects with `.taskmasterconfig` in the root will continue to work, but should be migrated to the new structure using `task-master migrate`.
|
||||
- **Management:** Use the `task-master models --setup` command (or `models` MCP tool) to interactively create and manage this file. You can also set specific models directly using `task-master models --set-<role>=<model_id>`, adding `--ollama` or `--openrouter` flags for custom models. Manual editing is possible but not recommended unless you understand the structure.
|
||||
@@ -173,6 +196,57 @@ node scripts/init.js
|
||||
|
||||
## Provider-Specific Configuration
|
||||
|
||||
### MCP (Model Context Protocol) Provider
|
||||
|
||||
The MCP provider enables Task Master to use MCP servers as AI providers. This is particularly useful when running Task Master within MCP-compatible development environments like Claude Desktop or Cursor.
|
||||
|
||||
1. **Prerequisites**:
|
||||
- An active MCP session with sampling capability
|
||||
- MCP client with sampling support (e.g. VS Code)
|
||||
- No API keys required (uses session-based authentication)
|
||||
|
||||
2. **Configuration**:
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "mcp",
|
||||
"modelId": "mcp-sampling"
|
||||
},
|
||||
"research": {
|
||||
"provider": "mcp",
|
||||
"modelId": "mcp-sampling"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Available Model IDs**:
|
||||
- `mcp-sampling` - General text generation using MCP client sampling (supports all roles)
|
||||
- `claude-3-5-sonnet-20241022` - High-performance model for general tasks (supports all roles)
|
||||
- `claude-3-opus-20240229` - Enhanced reasoning model for complex tasks (supports all roles)
|
||||
|
||||
4. **Features**:
|
||||
- ✅ **Text Generation**: Standard AI text generation via MCP sampling
|
||||
- ✅ **Object Generation**: Full schema-driven structured output generation
|
||||
- ✅ **PRD Parsing**: Parse Product Requirements Documents into structured tasks
|
||||
- ✅ **Task Creation**: AI-powered task creation with validation
|
||||
- ✅ **Session Management**: Automatic session detection and context handling
|
||||
- ✅ **Error Recovery**: Robust error handling and fallback mechanisms
|
||||
|
||||
5. **Usage Requirements**:
|
||||
- Must be running in an MCP context (session must be available)
|
||||
- Session must provide `clientCapabilities.sampling` capability
|
||||
|
||||
5. **Best Practices**:
|
||||
- Always configure a non-MCP fallback provider
|
||||
- Use `mcp` for main/research roles when in MCP environments
|
||||
- Test sampling capability before production use
|
||||
|
||||
6. **Troubleshooting**:
|
||||
- "MCP provider requires session context" → Ensure running in MCP environment
|
||||
- See the [MCP Provider Guide](./mcp-provider-guide.md) for detailed troubleshooting
|
||||
|
||||
### Google Vertex AI Configuration
|
||||
|
||||
Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration:
|
||||
|
||||
564
docs/mcp-provider-guide.md
Normal file
564
docs/mcp-provider-guide.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# MCP Provider Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Task Master provides a **unified MCP provider** for AI operations:
|
||||
|
||||
**MCP Provider** (`mcp`) - Modern AI SDK-compatible provider with full structured object generation support
|
||||
|
||||
The MCP provider enables Task Master to act as an MCP client, using MCP servers as AI providers alongside traditional API-based providers. This integration follows the existing provider pattern and supports all standard AI operations including structured object generation for PRD parsing and task creation.
|
||||
|
||||
## MCP Provider Features
|
||||
|
||||
The **MCP Provider** (`mcp`) provides:
|
||||
|
||||
✅ **Full AI SDK Compatibility** - Complete LanguageModelV1 interface implementation
|
||||
✅ **Structured Object Generation** - Schema-driven outputs for PRD parsing and task creation
|
||||
✅ **Enhanced Error Handling** - Robust JSON extraction and validation
|
||||
✅ **Session Management** - Automatic session detection and context handling
|
||||
✅ **Schema Validation** - Type-safe object generation with Zod validation
|
||||
|
||||
### Quick Setup
|
||||
|
||||
```bash
|
||||
# Set MCP provider for main role
|
||||
task-master models set-main --provider mcp --model claude-3-5-sonnet-20241022
|
||||
```
|
||||
|
||||
For detailed information, see [MCP Provider Documentation](mcp-provider.md).
|
||||
|
||||
## What is MCP Provider?
|
||||
|
||||
The MCP provider allows Task Master to:
|
||||
- Connect to MCP servers/tools as AI providers
|
||||
- Use session-based authentication instead of API keys
|
||||
- Map AI operations to MCP tool calls
|
||||
- Integrate with existing role-based provider assignment
|
||||
- Maintain compatibility with fallback chains
|
||||
- Support structured object generation for schema-driven features
|
||||
|
||||
## Configuration
|
||||
|
||||
### MCP Provider Setup
|
||||
|
||||
Add MCP provider to your `.taskmaster/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "mcp",
|
||||
"modelId": "claude-3-5-sonnet-20241022",
|
||||
"maxTokens": 50000,
|
||||
"temperature": 0.2
|
||||
},
|
||||
"research": {
|
||||
"provider": "mcp",
|
||||
"modelId": "claude-3-5-sonnet-20241022",
|
||||
"maxTokens": 8700,
|
||||
"temperature": 0.1
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-3-5-sonnet-20241022"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Models
|
||||
|
||||
**MCP Provider Models:**
|
||||
|
||||
- **`claude-3-5-sonnet-20241022`** - High-performance model for general tasks
|
||||
- **SWE Score**: 0.49
|
||||
- **Features**: Text + Object generation
|
||||
|
||||
- **`claude-3-opus-20240229`** - Enhanced reasoning model for complex tasks
|
||||
- **SWE Score**: 0.725
|
||||
- **Features**: Text + Object generation
|
||||
|
||||
- **`mcp-sampling`** - General text generation using MCP client sampling
|
||||
- **SWE Score**: null
|
||||
- **Roles**: Supports main, research, and fallback roles
|
||||
- **SWE Score**: 0.49
|
||||
- **Cost**: $0 (session-based)
|
||||
- **Max Tokens**: 200,000
|
||||
- **Supported Roles**: main, research, fallback
|
||||
- **Features**: Text + Object generation
|
||||
|
||||
- **`claude-3-opus-20240229`** - Enhanced reasoning model for complex tasks
|
||||
- **SWE Score**: 0.725
|
||||
- **Cost**: $0 (session-based)
|
||||
- **Max Tokens**: 200,000
|
||||
- **Supported Roles**: main, research, fallback
|
||||
- **Features**: Text + Object generation
|
||||
|
||||
**Basic MCP Provider Models:**
|
||||
|
||||
- **`mcp-sampling`** - General text generation using MCP client sampling
|
||||
- **`mcp-sampling`** - General text generation using MCP client sampling
|
||||
- **SWE Score**: null
|
||||
- **Roles**: Supports main, research, and fallback roles
|
||||
|
||||
### Model ID Format
|
||||
|
||||
MCP model IDs use a simple format:
|
||||
|
||||
- **`claude-3-5-sonnet-20241022`** - Uses Claude 3.5 Sonnet via MCP sampling
|
||||
- **`claude-3-opus-20240229`** - Uses Claude 3 Opus via MCP sampling
|
||||
- **`mcp-sampling`** - Uses MCP client's sampling capability for text generation
|
||||
|
||||
## Session Requirements
|
||||
|
||||
The MCP provider requires an active MCP session with sampling capabilities:
|
||||
|
||||
```javascript
|
||||
session: {
|
||||
clientCapabilities: {
|
||||
sampling: {} // Client supports sampling requests
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Text Generation
|
||||
|
||||
```javascript
|
||||
import { generateTextService } from './scripts/modules/ai-services-unified.js';
|
||||
|
||||
const result = await generateTextService({
|
||||
role: 'main',
|
||||
session: mcpSession, // Required for MCP provider
|
||||
prompt: 'Explain MCP integration',
|
||||
systemPrompt: 'You are a helpful AI assistant'
|
||||
});
|
||||
|
||||
console.log(result.text);
|
||||
```
|
||||
|
||||
### Structured Object Generation
|
||||
|
||||
```javascript
|
||||
import { generateObjectService } from './scripts/modules/ai-services-unified.js';
|
||||
|
||||
const result = await generateObjectService({
|
||||
role: 'main',
|
||||
session: mcpSession,
|
||||
prompt: 'Create a task breakdown',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tasks: {
|
||||
type: 'array',
|
||||
items: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(result.object);
|
||||
```
|
||||
|
||||
### Research Operations
|
||||
|
||||
```javascript
|
||||
const research = await generateTextService({
|
||||
role: 'research',
|
||||
session: mcpSession,
|
||||
prompt: 'Research the latest developments in AI',
|
||||
systemPrompt: 'You are a research assistant'
|
||||
});
|
||||
```
|
||||
|
||||
## CLI Integration
|
||||
|
||||
The MCP provider works seamlessly with Task Master CLI commands when running in an MCP context:
|
||||
|
||||
```bash
|
||||
# Generate tasks using MCP provider (if configured as main)
|
||||
task-master add-task "Implement user authentication"
|
||||
|
||||
# Research using MCP provider (if configured as research)
|
||||
task-master research "OAuth 2.0 best practices"
|
||||
|
||||
# Parse PRD using MCP provider
|
||||
task-master parse-prd requirements.txt
|
||||
```
|
||||
|
||||
## Architecture Details
|
||||
|
||||
### Provider Architecture
|
||||
**MCPProvider** (`mcp-server/src/providers/mcp-provider.js`)
|
||||
- Modern AI SDK-compliant provider for Task Master's MCP server
|
||||
- Auto-registers when MCP sessions connect to Task Master
|
||||
- Enables Task Master to use MCP sessions for AI operations
|
||||
- Supports both text generation and structured object generation
|
||||
|
||||
### Auto-Registration Process
|
||||
|
||||
When running as an MCP server, Task Master automatically:
|
||||
|
||||
```javascript
|
||||
// On MCP session connect
|
||||
server.on("connect", (event) => {
|
||||
// Check session capabilities
|
||||
if (session.clientCapabilities?.sampling) {
|
||||
// Create and register MCP provider
|
||||
const mcpProvider = new MCPProvider();
|
||||
mcpProvider.setSession(session);
|
||||
|
||||
// Auto-register with provider registry
|
||||
providerRegistry.registerProvider('mcp', mcpProvider);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This enables seamless self-referential AI operations within MCP contexts.
|
||||
|
||||
### Provider Pattern Integration
|
||||
|
||||
The MCP provider follows the same pattern as other providers:
|
||||
|
||||
```javascript
|
||||
class MCPProvider extends BaseAIProvider {
|
||||
// Implements generateText, generateObject
|
||||
// Uses session context instead of API keys
|
||||
// Maps operations to MCP sampling requests
|
||||
}
|
||||
```
|
||||
|
||||
### Session Detection
|
||||
|
||||
The provider automatically detects MCP sampling capability when sessions connect:
|
||||
|
||||
```javascript
|
||||
// On MCP session connect
|
||||
if (session.clientCapabilities?.sampling) {
|
||||
// Auto-register MCP provider for use
|
||||
const mcpProvider = new MCPProvider();
|
||||
mcpProvider.setSession(session);
|
||||
}
|
||||
```
|
||||
|
||||
### Sampling Integration
|
||||
|
||||
AI operations use MCP sampling with different levels of support:
|
||||
|
||||
- `generateText()` → MCP `requestSampling()` with messages (2-minute timeout) ✅ **Full Support**
|
||||
- `streamText()` → **Limited/No Support** ⚠️ See streaming limitations below
|
||||
- `generateObject()` → MCP `requestSampling()` with JSON schema instructions (2-minute timeout) ✅ **Full Support**
|
||||
|
||||
**Timeout Configuration**: All MCP sampling requests use a 2-minute (120,000ms) timeout to accommodate complex AI operations.
|
||||
|
||||
#### Streaming Text Limitations ⚠️
|
||||
|
||||
**Important**: The MCP provider has **no support** for text streaming:
|
||||
|
||||
**MCPProvider**:
|
||||
- **❌ No Streaming Support**: Throws error "MCP Provider does not support streaming text, use generateText instead"
|
||||
- **Solution**: Always use `generateText()` instead of `streamText()` with this provider
|
||||
|
||||
**Recommendation**: For streaming functionality, configure a non-MCP fallback provider (like Anthropic or OpenAI) in your fallback role.
|
||||
|
||||
### Error Handling
|
||||
|
||||
The MCP provider includes comprehensive error handling:
|
||||
|
||||
- Session validation errors (checks for `clientCapabilities.sampling`)
|
||||
- MCP sampling request failures
|
||||
- JSON parsing errors (for structured output)
|
||||
- Automatic fallback to other providers
|
||||
|
||||
### Best Practices
|
||||
|
||||
### 1. Configure Fallbacks
|
||||
|
||||
Always configure a non-MCP fallback provider, especially for streaming operations:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "mcp",
|
||||
"modelId": "mcp-sampling"
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "anthropic",
|
||||
"modelId": "claude-3-5-sonnet-20241022"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Avoid Streaming with MCP
|
||||
|
||||
**Do not use `streamTextService()` with MCP provider**. Use `generateTextService()` instead:
|
||||
|
||||
```javascript
|
||||
// ❌ Don't do this with MCP provider
|
||||
const result = await streamTextService({
|
||||
role: 'main', // MCP provider
|
||||
session: mcpSession,
|
||||
prompt: 'Generate content'
|
||||
});
|
||||
|
||||
// ✅ Do this instead
|
||||
const result = await generateTextService({
|
||||
role: 'main', // MCP provider
|
||||
session: mcpSession,
|
||||
prompt: 'Generate content'
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Session Management
|
||||
|
||||
Ensure your MCP session remains active throughout Task Master operations:
|
||||
|
||||
```javascript
|
||||
// Check session health before operations
|
||||
if (!session || !session.capabilities) {
|
||||
throw new Error('MCP session not available');
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Tool Availability
|
||||
|
||||
Verify required capabilities are available in your MCP session:
|
||||
|
||||
```javascript
|
||||
// Check session health and capabilities
|
||||
if (session && session.clientCapabilities && session.clientCapabilities.sampling) {
|
||||
console.log('MCP sampling available');
|
||||
} else {
|
||||
console.log('MCP sampling not available');
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Error Recovery
|
||||
|
||||
Handle MCP-specific errors gracefully:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const result = await generateTextService({
|
||||
role: 'main',
|
||||
session: mcpSession,
|
||||
prompt: 'Generate content'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('MCP')) {
|
||||
// Handle MCP-specific error
|
||||
console.log('MCP error, falling back to alternate provider');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"MCP provider requires session context"**
|
||||
- Ensure `session` parameter is passed to service calls
|
||||
- Verify session has proper structure
|
||||
- Check that you're running in an MCP environment
|
||||
|
||||
2. **"MCP session must have client sampling capabilities"**
|
||||
- Check that `session.clientCapabilities.sampling` exists
|
||||
- Verify session has `requestSampling()` method
|
||||
- Ensure MCP client supports sampling feature
|
||||
|
||||
3. **"MCP Provider does not support streaming text, use generateText instead"**
|
||||
- **Common Error**: Occurs when calling `streamTextService()` with MCP provider
|
||||
- **Solution**: Use `generateTextService()` instead of `streamTextService()`
|
||||
- **Alternative**: Configure a non-MCP fallback provider for streaming operations
|
||||
|
||||
4. **"MCP sampling failed"** or **Timeout errors**
|
||||
- Check MCP client is responding to sampling requests
|
||||
- Verify session is still active and connected
|
||||
- Consider if request complexity requires longer processing time
|
||||
- Check for network connectivity issues
|
||||
|
||||
5. **"Model ID is required for MCP Remote Provider"**
|
||||
- Ensure `modelId` is specified in configuration
|
||||
- Use `mcp-sampling` as the standard model ID
|
||||
- Verify provider configuration is properly loaded
|
||||
|
||||
6. **Auto-registration failures**
|
||||
- Check that MCP session has required sampling capabilities
|
||||
- Verify server event listeners are properly configured
|
||||
- Look for provider registry initialization issues
|
||||
|
||||
### Streaming-Related Issues
|
||||
|
||||
**Error**: `streamTextService()` calls fail with MCP provider
|
||||
**Cause**: MCP provider has no streaming support
|
||||
**Solutions**:
|
||||
- Use `generateTextService()` for all MCP-based text generation
|
||||
- Configure non-MCP fallback providers for streaming requirements
|
||||
- Check your provider configuration to ensure fallback chain includes streaming-capable providers
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging to see MCP provider operations:
|
||||
|
||||
```javascript
|
||||
// Set debug flag in config or environment
|
||||
process.env.DEBUG = 'true';
|
||||
|
||||
// Or in .taskmasterconfig
|
||||
{
|
||||
"debug": true,
|
||||
"models": { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Testing MCP Integration
|
||||
|
||||
Test MCP provider functionality:
|
||||
|
||||
```javascript
|
||||
// Check if MCP provider is properly registered
|
||||
import { MCPProvider } from './mcp-server/src/providers/mcp-provider.js';
|
||||
|
||||
// Test session capabilities
|
||||
if (session && session.clientCapabilities && session.clientCapabilities.sampling) {
|
||||
console.log('MCP sampling available');
|
||||
|
||||
// Test provider creation
|
||||
const provider = new MCPProvider();
|
||||
provider.setSession(session);
|
||||
console.log('MCP provider created successfully');
|
||||
} else {
|
||||
console.log('MCP session lacks required capabilities');
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Development Tools
|
||||
|
||||
### VS Code with MCP Extension
|
||||
|
||||
When using Task Master in VS Code with MCP support:
|
||||
|
||||
1. Configure Task Master MCP server in your `.vscode/mcp.json`
|
||||
2. Set MCP provider as main/research in `.taskmaster/config.json`
|
||||
3. Benefit from integrated AI assistance within your development workflow
|
||||
4. Use Task Master tools directly from VS Code's MCP interface
|
||||
|
||||
**Example VS Code MCP Configuration:**
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"task-master-dev": {
|
||||
"command": "node",
|
||||
"args": ["mcp-server/server.js"],
|
||||
"cwd": "/path/to/your/task-master-project",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"ANTHROPIC_API_KEY": "${env:ANTHROPIC_API_KEY}",
|
||||
"TASK_MASTER_PROJECT_ROOT": "/path/to/your/project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
When using Task Master through Claude Desktop's MCP integration:
|
||||
|
||||
1. Configure Task Master as MCP provider in Claude Desktop
|
||||
2. Use MCP provider for AI operations within Task Master
|
||||
3. Benefit from nested MCP tool calling capabilities
|
||||
|
||||
### Cursor and Other MCP Clients
|
||||
|
||||
The MCP provider works with any MCP-compatible development environment:
|
||||
|
||||
1. Ensure your IDE has MCP client capabilities
|
||||
2. Configure Task Master MCP server endpoint
|
||||
3. Use MCP provider for enhanced AI-driven development
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Tool Mapping
|
||||
|
||||
Advanced users can use MCP sampling for all roles:
|
||||
|
||||
```javascript
|
||||
// MCP sampling for all roles
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "mcp",
|
||||
"modelId": "mcp-sampling"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Role-Specific Configuration
|
||||
|
||||
Configure MCP sampling for different roles:
|
||||
|
||||
```json
|
||||
{
|
||||
"models": {
|
||||
"main": {
|
||||
"provider": "mcp",
|
||||
"modelId": "mcp-sampling"
|
||||
},
|
||||
"research": {
|
||||
"provider": "mcp",
|
||||
"modelId": "mcp-sampling"
|
||||
},
|
||||
"fallback": {
|
||||
"provider": "mcp",
|
||||
"modelId": "backup-server:simple-generation"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Reference
|
||||
|
||||
### MCPProvider Methods
|
||||
|
||||
- `generateText(params)` - Generate text using MCP sampling ✅ **Supported**
|
||||
- `streamText(params)` - Stream text ❌ **Not supported** (throws error)
|
||||
- `generateObject(params)` - Generate structured objects ✅ **Supported**
|
||||
- `setSession(session)` - Update provider session
|
||||
- `validateAuth(params)` - Validate session capabilities
|
||||
- `getClient()` - Returns null (not applicable for MCP)
|
||||
|
||||
### Required Parameters
|
||||
|
||||
All MCP operations require:
|
||||
- `session` - Active MCP session object (auto-provided when registered)
|
||||
- `modelId` - MCP model identifier (typically "mcp-sampling")
|
||||
- `messages` - Array of message objects
|
||||
|
||||
### Optional Parameters
|
||||
|
||||
- `temperature` - Creativity control (if supported by MCP client)
|
||||
- `maxTokens` - Maximum response length (if supported)
|
||||
- `schema` - JSON schema for structured output (generateObject only)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Session Security**: MCP sessions should be properly authenticated
|
||||
2. **Server Validation**: Only connect to trusted MCP servers
|
||||
3. **Data Privacy**: Ensure MCP clients handle data according to your privacy requirements
|
||||
4. **Error Exposure**: Be careful not to expose sensitive session information in error messages
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements for MCP provider:
|
||||
|
||||
1. **Native Streaming Support** - True streaming for compatible MCP clients (requires MCP protocol updates)
|
||||
2. **Enhanced Session Monitoring** - Automatic session validation and recovery
|
||||
3. **Performance Optimization** - Caching and connection pooling
|
||||
4. **Advanced Error Recovery** - Intelligent retry and fallback strategies
|
||||
|
||||
**Note**: True streaming support depends on future MCP protocol enhancements. Current implementation provides text generation without streaming capabilities.
|
||||
350
docs/mcp-provider.md
Normal file
350
docs/mcp-provider.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# MCP Provider Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP Provider creates a modern AI SDK-compliant custom provider that integrates with the existing Task Master MCP server infrastructure. This provider enables AI operations through MCP session sampling while following modern AI SDK patterns and **includes full support for structured object generation (generateObject)** for schema-driven features like PRD parsing and task creation.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
1. **MCPProvider** (`mcp-server/src/providers/mcp-provider.js`)
|
||||
- Main provider class following Claude Code pattern
|
||||
- Session-based provider (no API key required)
|
||||
- Registers with provider registry on MCP server connect
|
||||
|
||||
2. **AI SDK Implementation** (`mcp-server/src/custom-sdk/`)
|
||||
- `index.js` - Provider factory function
|
||||
- `language-model.js` - LanguageModelV1 implementation with **doGenerateObject support**
|
||||
- `message-converter.js` - Format conversion utilities
|
||||
- `json-extractor.js` - **NEW**: Robust JSON extraction from AI responses
|
||||
- `schema-converter.js` - **NEW**: Schema-to-instructions conversion utility
|
||||
- `errors.js` - Error handling and mapping
|
||||
|
||||
3. **Integration Points**
|
||||
- MCP Server registration (`mcp-server/src/index.js`)
|
||||
- AI Services integration (`scripts/modules/ai-services-unified.js`)
|
||||
- Model configuration (`scripts/modules/supported-models.json`)
|
||||
|
||||
### Session Flow
|
||||
|
||||
```
|
||||
MCP Client Connect → MCP Server → registerRemoteProvider()
|
||||
↓
|
||||
MCPRemoteProvider (existing)
|
||||
MCPProvider
|
||||
↓
|
||||
Provider Registry
|
||||
↓
|
||||
AI Services Layer
|
||||
↓
|
||||
Text Generation + Object Generation
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Provider Registration
|
||||
|
||||
The MCP server registers **both** providers when a client connects:
|
||||
|
||||
```javascript
|
||||
// mcp-server/src/index.js
|
||||
registerRemoteProvider(session) {
|
||||
if (session?.clientCapabilities?.sampling) {
|
||||
// Register existing provider
|
||||
// Register unified MCP provider
|
||||
const mcpProvider = new MCPProvider();
|
||||
mcpProvider.setSession(session);
|
||||
|
||||
const providerRegistry = ProviderRegistry.getInstance();
|
||||
providerRegistry.registerProvider('mcp', mcpProvider);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AI Services Integration
|
||||
|
||||
The AI services layer includes the new provider:
|
||||
|
||||
```javascript
|
||||
// scripts/modules/ai-services-unified.js
|
||||
const PROVIDERS = {
|
||||
// ... other providers
|
||||
'mcp': () => {
|
||||
const providerRegistry = ProviderRegistry.getInstance();
|
||||
return providerRegistry.getProvider('mcp');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Message Conversion
|
||||
|
||||
The provider converts between AI SDK and MCP formats:
|
||||
|
||||
```javascript
|
||||
// AI SDK prompt → MCP sampling format
|
||||
const { messages, systemPrompt } = convertToMCPFormat(options.prompt);
|
||||
|
||||
// MCP response → AI SDK format
|
||||
const result = convertFromMCPFormat(response);
|
||||
```
|
||||
|
||||
## Structured Object Generation (generateObject)
|
||||
|
||||
### Overview
|
||||
|
||||
The MCP Provider includes full support for structured object generation, enabling schema-driven features like PRD parsing, task creation, and any operations requiring validated JSON outputs.
|
||||
|
||||
### Architecture
|
||||
|
||||
The generateObject implementation includes:
|
||||
|
||||
1. **Schema-to-Instructions Conversion** (`schema-converter.js`)
|
||||
- Converts Zod schemas to natural language instructions
|
||||
- Generates example outputs to guide AI responses
|
||||
- Handles complex nested schemas and validation requirements
|
||||
|
||||
2. **JSON Extraction Pipeline** (`json-extractor.js`)
|
||||
- Multiple extraction strategies for robust JSON parsing
|
||||
- Handles code blocks, malformed JSON, and various response formats
|
||||
- Fallback mechanisms for maximum reliability
|
||||
|
||||
3. **Validation System**
|
||||
- Complete schema validation using Zod
|
||||
- Detailed error reporting for failed validations
|
||||
- Type-safe object generation
|
||||
|
||||
### Implementation Details
|
||||
|
||||
#### doGenerateObject Method
|
||||
|
||||
The `MCPLanguageModel` class implements the AI SDK's `doGenerateObject` method:
|
||||
|
||||
```javascript
|
||||
async doGenerateObject({ schema, objectName, prompt, ...options }) {
|
||||
// Convert schema to instructions
|
||||
const instructions = convertSchemaToInstructions(schema, objectName);
|
||||
|
||||
// Enhance prompt with structured output requirements
|
||||
const enhancedPrompt = enhancePromptForObjectGeneration(prompt, instructions);
|
||||
|
||||
// Generate response via MCP sampling
|
||||
const response = await this.doGenerate({ prompt: enhancedPrompt, ...options });
|
||||
|
||||
// Extract and validate JSON
|
||||
const extractedJson = extractJsonFromResponse(response.text);
|
||||
const validatedObject = schema.parse(extractedJson);
|
||||
|
||||
return {
|
||||
object: validatedObject,
|
||||
usage: response.usage,
|
||||
finishReason: response.finishReason
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### AI SDK Compatibility
|
||||
|
||||
The provider includes required properties for AI SDK object generation:
|
||||
|
||||
```javascript
|
||||
class MCPLanguageModel {
|
||||
get defaultObjectGenerationMode() {
|
||||
return 'tool';
|
||||
}
|
||||
|
||||
get supportsStructuredOutputs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ... doGenerateObject implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### PRD Parsing
|
||||
|
||||
```javascript
|
||||
import { z } from 'zod';
|
||||
|
||||
const taskSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
priority: z.enum(['high', 'medium', 'low']),
|
||||
dependencies: z.array(z.number()).optional()
|
||||
});
|
||||
|
||||
const result = await generateObject({
|
||||
model: mcpModel,
|
||||
schema: taskSchema,
|
||||
prompt: 'Parse this PRD section into a task: [PRD content]'
|
||||
});
|
||||
|
||||
console.log(result.object); // Validated task object
|
||||
```
|
||||
|
||||
#### Task Creation
|
||||
|
||||
```javascript
|
||||
const taskCreationSchema = z.object({
|
||||
task: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
details: z.string(),
|
||||
testStrategy: z.string(),
|
||||
priority: z.enum(['high', 'medium', 'low']),
|
||||
dependencies: z.array(z.number()).optional()
|
||||
})
|
||||
});
|
||||
|
||||
const result = await generateObject({
|
||||
model: mcpModel,
|
||||
schema: taskCreationSchema,
|
||||
prompt: 'Create a comprehensive task for implementing user authentication'
|
||||
});
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
The implementation provides comprehensive error handling:
|
||||
|
||||
- **Schema Validation Errors**: Detailed Zod validation messages
|
||||
- **JSON Extraction Failures**: Fallback strategies and clear error reporting
|
||||
- **MCP Communication Errors**: Proper error mapping and recovery
|
||||
- **Timeout Handling**: Configurable timeouts for long-running operations
|
||||
|
||||
### Testing
|
||||
|
||||
The generateObject functionality is fully tested:
|
||||
|
||||
```bash
|
||||
# Test object generation
|
||||
npm test -- --grep "generateObject"
|
||||
|
||||
# Test with actual MCP session
|
||||
node test-object-generation.js
|
||||
```
|
||||
|
||||
### Supported Features
|
||||
|
||||
✅ **Schema Conversion**: Zod schemas → Natural language instructions
|
||||
✅ **JSON Extraction**: Multiple strategies for robust parsing
|
||||
✅ **Validation**: Complete schema validation with error reporting
|
||||
✅ **Error Recovery**: Fallback mechanisms for failed extractions
|
||||
✅ **Type Safety**: Full TypeScript support with inferred types
|
||||
✅ **AI SDK Compliance**: Complete LanguageModelV1 interface implementation
|
||||
|
||||
## Usage
|
||||
|
||||
### Configuration
|
||||
|
||||
Add to supported models configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": [
|
||||
{
|
||||
"id": "claude-3-5-sonnet-20241022",
|
||||
"swe_score": 0.623,
|
||||
"cost_per_1m_tokens": { "input": 0, "output": 0 },
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 200000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Usage
|
||||
|
||||
```bash
|
||||
# Set provider for main role
|
||||
tm models set-main --provider mcp --model claude-3-5-sonnet-20241022
|
||||
|
||||
# Use in task operations
|
||||
tm add-task "Create user authentication system"
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```javascript
|
||||
const provider = registry.getProvider('mcp');
|
||||
if (provider && provider.hasValidSession()) {
|
||||
const client = provider.getClient({ temperature: 0.7 });
|
||||
const model = client({ modelId: 'claude-3-5-sonnet-20241022' });
|
||||
|
||||
const result = await model.doGenerate({
|
||||
prompt: [
|
||||
{ role: 'user', content: 'Hello!' }
|
||||
]
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Component Tests
|
||||
|
||||
```bash
|
||||
# Test individual components
|
||||
node test-mcp-components.js
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
1. Start MCP server
|
||||
2. Connect Claude client
|
||||
3. Verify both providers are registered
|
||||
4. Test AI operations through mcp provider
|
||||
|
||||
### Validation Checklist
|
||||
|
||||
- ✅ Provider creation and initialization
|
||||
- ✅ Registry integration
|
||||
- ✅ Session management
|
||||
- ✅ Message conversion
|
||||
- ✅ Error handling
|
||||
- ✅ AI Services integration
|
||||
- ✅ Model configuration
|
||||
|
||||
## Key Benefits
|
||||
|
||||
1. **AI SDK Compliance** - Full LanguageModelV1 implementation
|
||||
2. **Session Integration** - Leverages existing MCP session infrastructure
|
||||
3. **Registry Pattern** - Uses provider registry for discovery
|
||||
4. **Backward Compatibility** - Coexists with existing MCPRemoteProvider
|
||||
5. **Future Ready** - Supports AI SDK features and patterns
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Provider Not Found
|
||||
|
||||
```
|
||||
Error: Provider "mcp" not found in registry
|
||||
```
|
||||
|
||||
**Solution**: Ensure MCP server is running and client is connected
|
||||
|
||||
### Session Errors
|
||||
|
||||
```
|
||||
Error: MCP Provider requires active MCP session
|
||||
```
|
||||
|
||||
**Solution**: Check MCP client connection and session capabilities
|
||||
|
||||
### Sampling Errors
|
||||
|
||||
```
|
||||
Error: MCP session must have client sampling capabilities
|
||||
```
|
||||
|
||||
**Solution**: Verify MCP client supports sampling operations
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Performance Optimization** - Add caching and connection pooling
|
||||
2. **Enhanced Streaming** - Implement native streaming if MCP supports it
|
||||
3. **Tool Integration** - Add support for function calling through MCP tools
|
||||
4. **Monitoring** - Add metrics and logging for provider usage
|
||||
5. **Documentation** - Update user guides and API documentation
|
||||
@@ -40,6 +40,7 @@
|
||||
| perplexity | sonar-reasoning | 0.211 | 1 | 5 |
|
||||
| xai | grok-3 | — | 3 | 15 |
|
||||
| xai | grok-3-fast | — | 5 | 25 |
|
||||
| mcp | mcp-sampling | - | 0 | 0 |
|
||||
| ollama | devstral:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:14b | — | 0 | 0 |
|
||||
@@ -69,7 +70,6 @@
|
||||
| openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 |
|
||||
| openrouter | mistralai/mistral-small-3.1-24b-instruct:free | — | 0 | 0 |
|
||||
| openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 |
|
||||
| openrouter | mistralai/devstral-small | — | 0.1 | 0.3 |
|
||||
| openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 |
|
||||
| openrouter | thudm/glm-4-32b:free | — | 0 | 0 |
|
||||
| groq | llama-3.3-70b-versatile | 0.55 | 0.59 | 0.79 |
|
||||
@@ -113,6 +113,7 @@
|
||||
| groq | deepseek-r1-distill-llama-70b | 0.52 | 0.75 | 0.99 |
|
||||
| claude-code | opus | 0.725 | 0 | 0 |
|
||||
| claude-code | sonnet | 0.727 | 0 | 0 |
|
||||
| mcp | mcp-sampling | - | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 |
|
||||
| gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 |
|
||||
|
||||
@@ -147,6 +148,7 @@
|
||||
| perplexity | sonar-reasoning | 0.211 | 1 | 5 |
|
||||
| xai | grok-3 | — | 3 | 15 |
|
||||
| xai | grok-3-fast | — | 5 | 25 |
|
||||
| mcp | mcp-sampling | - | 0 | 0 |
|
||||
| ollama | devstral:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:latest | — | 0 | 0 |
|
||||
| ollama | qwen3:14b | — | 0 | 0 |
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import logger from './logger.js';
|
||||
import { registerTaskMasterTools } from './tools/index.js';
|
||||
import ProviderRegistry from '../../src/provider-registry/index.js';
|
||||
import { MCPProvider } from './providers/mcp-provider.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
@@ -65,6 +67,17 @@ class TaskMasterMCPServer {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
this.server.on('connect', (event) => {
|
||||
event.session.server.sendLoggingMessage({
|
||||
data: {
|
||||
context: event.session.context,
|
||||
message: `MCP Server connected: ${event.session.name}`
|
||||
},
|
||||
level: 'info'
|
||||
});
|
||||
this.registerRemoteProvider(event.session);
|
||||
});
|
||||
|
||||
// Start the FastMCP server with increased timeout
|
||||
await this.server.start({
|
||||
transportType: 'stdio',
|
||||
@@ -74,6 +87,52 @@ class TaskMasterMCPServer {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register both MCP providers with the provider registry
|
||||
*/
|
||||
registerRemoteProvider(session) {
|
||||
// Check if the server has at least one session
|
||||
if (session) {
|
||||
// Make sure session has required capabilities
|
||||
if (!session.clientCapabilities || !session.clientCapabilities.sampling) {
|
||||
session.server.sendLoggingMessage({
|
||||
data: {
|
||||
context: session.context,
|
||||
message: `MCP session missing required sampling capabilities, providers not registered`
|
||||
},
|
||||
level: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Register MCP provider with the Provider Registry
|
||||
|
||||
// Register the unified MCP provider
|
||||
const mcpProvider = new MCPProvider();
|
||||
mcpProvider.setSession(session);
|
||||
|
||||
// Register provider with the registry
|
||||
const providerRegistry = ProviderRegistry.getInstance();
|
||||
providerRegistry.registerProvider('mcp', mcpProvider);
|
||||
|
||||
session.server.sendLoggingMessage({
|
||||
data: {
|
||||
context: session.context,
|
||||
message: `MCP Server connected`
|
||||
},
|
||||
level: 'info'
|
||||
});
|
||||
} else {
|
||||
session.server.sendLoggingMessage({
|
||||
data: {
|
||||
context: session.context,
|
||||
message: `No MCP sessions available, providers not registered`
|
||||
},
|
||||
level: 'warn'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the MCP server
|
||||
*/
|
||||
|
||||
84
mcp-server/src/providers/mcp-provider.js
Normal file
84
mcp-server/src/providers/mcp-provider.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* mcp-server/src/providers/mcp-provider.js
|
||||
*
|
||||
* Implementation for MCP custom AI SDK provider that integrates with
|
||||
* the existing MCP server infrastructure and provider registry.
|
||||
* Follows the Claude Code provider pattern for session-based providers.
|
||||
*/
|
||||
|
||||
import { createMCP } from '../custom-sdk/index.js';
|
||||
import { BaseAIProvider } from '../../../src/ai-providers/base-provider.js';
|
||||
|
||||
export class MCPProvider extends BaseAIProvider {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'mcp';
|
||||
this.session = null; // MCP server session object
|
||||
}
|
||||
|
||||
getRequiredApiKeyName() {
|
||||
return 'MCP_API_KEY';
|
||||
}
|
||||
|
||||
isRequiredApiKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override validateAuth to validate MCP session instead of API key
|
||||
* @param {object} params - Parameters to validate
|
||||
*/
|
||||
validateAuth(params) {
|
||||
// Validate MCP session instead of API key
|
||||
if (!this.session) {
|
||||
throw new Error('MCP Provider requires active MCP session');
|
||||
}
|
||||
|
||||
if (!this.session.clientCapabilities?.sampling) {
|
||||
throw new Error('MCP session must have client sampling capabilities');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns an MCP AI SDK client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
* @returns {Function} MCP AI SDK client function
|
||||
* @throws {Error} If initialization fails
|
||||
*/
|
||||
getClient(params) {
|
||||
try {
|
||||
// Pass MCP session to AI SDK implementation
|
||||
return createMCP({
|
||||
session: this.session,
|
||||
defaultSettings: {
|
||||
temperature: params.temperature,
|
||||
maxTokens: params.maxTokens
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError('client initialization', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called by MCP server on connect events
|
||||
* @param {object} session - MCP session object
|
||||
*/
|
||||
setSession(session) {
|
||||
this.session = session;
|
||||
|
||||
if (!session) {
|
||||
this.logger?.warn('Set null session on MCP Provider');
|
||||
} else {
|
||||
this.logger?.debug('Updated MCP Provider session');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session status
|
||||
* @returns {boolean} True if session is available and valid
|
||||
*/
|
||||
hasValidSession() {
|
||||
return !!(this.session && this.session.clientCapabilities?.sampling);
|
||||
}
|
||||
}
|
||||
125
package-lock.json
generated
125
package-lock.json
generated
@@ -32,7 +32,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.21.2",
|
||||
"fastmcp": "^2.2.2",
|
||||
"fastmcp": "^3.5.0",
|
||||
"figlet": "^1.8.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"gpt-tokens": "^1.3.14",
|
||||
@@ -46,7 +46,8 @@
|
||||
"openai": "^4.89.0",
|
||||
"ora": "^8.2.0",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"bin": {
|
||||
"task-master": "bin/task-master.js",
|
||||
@@ -7083,6 +7084,7 @@
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
@@ -7097,6 +7099,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7106,6 +7109,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -7121,12 +7125,14 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7136,6 +7142,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -7150,6 +7157,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -7162,6 +7170,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -8102,6 +8111,36 @@
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express/node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -8188,9 +8227,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fastmcp": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-2.2.4.tgz",
|
||||
"integrity": "sha512-jDO0yZpZGdA809WGszsK2jmC68sklbSmXMpt7NedCb7MV2SCzmCCYnCR59DNtDwhSSZF2HIDKo6pLi3+2PwImg==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fastmcp/-/fastmcp-3.5.0.tgz",
|
||||
"integrity": "sha512-80OeYVFCqusrOFPNxm8uJ5G/ZaKMasygZTLvZReZ5o6iCvDBwFR/S485Is3jY0128aAaIyYx9eAgWjBUQUwoYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
@@ -8198,7 +8237,7 @@
|
||||
"execa": "^9.6.0",
|
||||
"file-type": "^21.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"mcp-proxy": "^3.0.3",
|
||||
"mcp-proxy": "^5.0.0",
|
||||
"strict-event-emitter-types": "^2.0.0",
|
||||
"undici": "^7.10.0",
|
||||
"uri-templates": "^0.2.0",
|
||||
@@ -11412,19 +11451,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mcp-proxy": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-3.3.0.tgz",
|
||||
"integrity": "sha512-xyFKQEZ64HC7lxScBHjb5fxiPoyJjjkPhwH5hWUT0oL/ttCpMGZDJrYZRGFKVJiLLkrZPAkHnMGkI+WMlyD/cg==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mcp-proxy/-/mcp-proxy-5.1.1.tgz",
|
||||
"integrity": "sha512-7FTIRIdHjZL7/sRBQ2/FiyQZDV3tJGBANJ9UV5VTiSaOOkDs8F1boY+EP4X/Blcme8eWVFjzmfx9LqdXA/ZHRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.11.4",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"eventsource": "^4.0.0",
|
||||
"yargs": "^17.7.2"
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"mcp-proxy": "dist/bin/mcp-proxy.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mcp-proxy/node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/mcp-proxy/node_modules/eventsource": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.0.0.tgz",
|
||||
@@ -11437,6 +11490,49 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mcp-proxy/node_modules/wrap-ansi": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
|
||||
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/mcp-proxy/node_modules/yargs": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/mcp-proxy/node_modules/yargs-parser": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -14389,6 +14485,7 @@
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
@@ -14407,6 +14504,7 @@
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -14416,6 +14514,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -14425,12 +14524,14 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -14440,6 +14541,7 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -14454,6 +14556,7 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.21.2",
|
||||
"fastmcp": "^2.2.2",
|
||||
"fastmcp": "^3.5.0",
|
||||
"figlet": "^1.8.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"gpt-tokens": "^1.3.14",
|
||||
@@ -76,7 +76,8 @@
|
||||
"openai": "^4.89.0",
|
||||
"ora": "^8.2.0",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-code": "^1.0.25",
|
||||
|
||||
@@ -51,6 +51,9 @@ import {
|
||||
GeminiCliProvider
|
||||
} from '../../src/ai-providers/index.js';
|
||||
|
||||
// Import the provider registry
|
||||
import ProviderRegistry from '../../src/provider-registry/index.js';
|
||||
|
||||
// Create provider instances
|
||||
const PROVIDERS = {
|
||||
anthropic: new AnthropicAIProvider(),
|
||||
@@ -67,6 +70,23 @@ const PROVIDERS = {
|
||||
'gemini-cli': new GeminiCliProvider()
|
||||
};
|
||||
|
||||
function _getProvider(providerName) {
|
||||
// First check the static PROVIDERS object
|
||||
if (PROVIDERS[providerName]) {
|
||||
return PROVIDERS[providerName];
|
||||
}
|
||||
|
||||
// If not found, check the provider registry
|
||||
const providerRegistry = ProviderRegistry.getInstance();
|
||||
if (providerRegistry.hasProvider(providerName)) {
|
||||
log('debug', `Provider "${providerName}" found in dynamic registry`);
|
||||
return providerRegistry.getProvider(providerName);
|
||||
}
|
||||
|
||||
// Provider not found in either location
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to get cost for a specific model
|
||||
function _getCostForModel(providerName, modelId) {
|
||||
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
|
||||
@@ -231,44 +251,26 @@ function _extractErrorMessage(error) {
|
||||
* @throws {Error} If a required API key is missing.
|
||||
*/
|
||||
function _resolveApiKey(providerName, session, projectRoot = null) {
|
||||
// Claude Code doesn't require an API key
|
||||
if (providerName === 'claude-code') {
|
||||
return 'claude-code-no-key-required';
|
||||
}
|
||||
|
||||
// Gemini CLI can work without an API key (uses CLI auth)
|
||||
if (providerName === 'gemini-cli') {
|
||||
const apiKey = resolveEnvVariable('GEMINI_API_KEY', session, projectRoot);
|
||||
return apiKey || 'gemini-cli-no-key-required';
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
openai: 'OPENAI_API_KEY',
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
google: 'GOOGLE_API_KEY',
|
||||
perplexity: 'PERPLEXITY_API_KEY',
|
||||
mistral: 'MISTRAL_API_KEY',
|
||||
azure: 'AZURE_OPENAI_API_KEY',
|
||||
openrouter: 'OPENROUTER_API_KEY',
|
||||
xai: 'XAI_API_KEY',
|
||||
ollama: 'OLLAMA_API_KEY',
|
||||
bedrock: 'AWS_ACCESS_KEY_ID',
|
||||
vertex: 'GOOGLE_API_KEY',
|
||||
'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
|
||||
'gemini-cli': 'GEMINI_API_KEY'
|
||||
};
|
||||
|
||||
const envVarName = keyMap[providerName];
|
||||
if (!envVarName) {
|
||||
// Get provider instance
|
||||
const provider = _getProvider(providerName);
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`Unknown provider '${providerName}' for API key resolution.`
|
||||
);
|
||||
}
|
||||
|
||||
// All providers must implement getRequiredApiKeyName()
|
||||
const envVarName = provider.getRequiredApiKeyName();
|
||||
|
||||
// If envVarName is null (like for MCP), return null directly
|
||||
if (envVarName === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const apiKey = resolveEnvVariable(envVarName, session, projectRoot);
|
||||
|
||||
// Special handling for providers that can use alternative auth
|
||||
if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
|
||||
// Special handling for providers that can use alternative auth or no API key
|
||||
if (!provider.isRequiredApiKey()) {
|
||||
return apiKey || null;
|
||||
}
|
||||
|
||||
@@ -455,7 +457,7 @@ async function _unifiedServiceRunner(serviceType, params) {
|
||||
}
|
||||
|
||||
// Get provider instance
|
||||
provider = PROVIDERS[providerName?.toLowerCase()];
|
||||
provider = _getProvider(providerName?.toLowerCase());
|
||||
if (!provider) {
|
||||
log(
|
||||
'warn',
|
||||
|
||||
@@ -592,6 +592,7 @@ function isApiKeySet(providerName, session = null, projectRoot = null) {
|
||||
const providersWithoutApiKeys = [
|
||||
CUSTOM_PROVIDERS.OLLAMA,
|
||||
CUSTOM_PROVIDERS.BEDROCK,
|
||||
CUSTOM_PROVIDERS.MCP,
|
||||
CUSTOM_PROVIDERS.GEMINI_CLI
|
||||
];
|
||||
|
||||
@@ -890,7 +891,8 @@ function getBaseUrlForRole(role, explicitRoot = null) {
|
||||
export const providersWithoutApiKeys = [
|
||||
CUSTOM_PROVIDERS.OLLAMA,
|
||||
CUSTOM_PROVIDERS.BEDROCK,
|
||||
CUSTOM_PROVIDERS.GEMINI_CLI
|
||||
CUSTOM_PROVIDERS.GEMINI_CLI,
|
||||
CUSTOM_PROVIDERS.MCP
|
||||
];
|
||||
|
||||
export {
|
||||
|
||||
@@ -3,25 +3,37 @@
|
||||
{
|
||||
"id": "us.anthropic.claude-3-haiku-20240307-v1:0",
|
||||
"swe_score": 0.4,
|
||||
"cost_per_1m_tokens": { "input": 0.25, "output": 1.25 },
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0.25,
|
||||
"output": 1.25
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "us.anthropic.claude-3-opus-20240229-v1:0",
|
||||
"swe_score": 0.725,
|
||||
"cost_per_1m_tokens": { "input": 15, "output": 75 },
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 15,
|
||||
"output": 75
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
},
|
||||
{
|
||||
"id": "us.anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
"swe_score": 0.49,
|
||||
"cost_per_1m_tokens": { "input": 3, "output": 15 },
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 3,
|
||||
"output": 15
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
},
|
||||
{
|
||||
"id": "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
"swe_score": 0.49,
|
||||
"cost_per_1m_tokens": { "input": 3, "output": 15 },
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 3,
|
||||
"output": 15
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
},
|
||||
{
|
||||
@@ -37,19 +49,28 @@
|
||||
{
|
||||
"id": "us.anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
"swe_score": 0.4,
|
||||
"cost_per_1m_tokens": { "input": 0.8, "output": 4 },
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0.8,
|
||||
"output": 4
|
||||
},
|
||||
"allowed_roles": ["main", "fallback"]
|
||||
},
|
||||
{
|
||||
"id": "us.anthropic.claude-opus-4-20250514-v1:0",
|
||||
"swe_score": 0.725,
|
||||
"cost_per_1m_tokens": { "input": 15, "output": 75 },
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 15,
|
||||
"output": 75
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
},
|
||||
{
|
||||
"id": "us.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
"swe_score": 0.727,
|
||||
"cost_per_1m_tokens": { "input": 3, "output": 15 },
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 3,
|
||||
"output": 15
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"]
|
||||
},
|
||||
{
|
||||
@@ -806,6 +827,18 @@
|
||||
"max_tokens": 64000
|
||||
}
|
||||
],
|
||||
"mcp": [
|
||||
{
|
||||
"id": "mcp-sampling",
|
||||
"swe_score": null,
|
||||
"cost_per_1m_tokens": {
|
||||
"input": 0,
|
||||
"output": 0
|
||||
},
|
||||
"allowed_roles": ["main", "fallback", "research"],
|
||||
"max_tokens": 100000
|
||||
}
|
||||
],
|
||||
"gemini-cli": [
|
||||
{
|
||||
"id": "gemini-2.5-pro",
|
||||
|
||||
@@ -23,6 +23,14 @@ export class AnthropicAIProvider extends BaseAIProvider {
|
||||
this.name = 'Anthropic';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the Anthropic API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'ANTHROPIC_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns an Anthropic client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
|
||||
@@ -12,6 +12,14 @@ export class AzureProvider extends BaseAIProvider {
|
||||
this.name = 'Azure OpenAI';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the Azure OpenAI API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'AZURE_OPENAI_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Azure-specific authentication parameters
|
||||
* @param {object} params - Parameters to validate
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateObject, generateText, streamText } from 'ai';
|
||||
import { log } from '../../scripts/modules/index.js';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Base class for all AI providers
|
||||
@@ -96,6 +96,24 @@ export class BaseAIProvider {
|
||||
throw new Error('getClient must be implemented by provider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the API key is required
|
||||
* @abstract
|
||||
* @returns {boolean} if the API key is required, defaults to true
|
||||
*/
|
||||
isRequiredApiKey() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required API key environment variable name
|
||||
* @abstract
|
||||
* @returns {string|null} The environment variable name, or null if no API key is required
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
throw new Error('getRequiredApiKeyName must be implemented by provider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates text using the provider's model
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,19 @@ export class BedrockAIProvider extends BaseAIProvider {
|
||||
this.name = 'Bedrock';
|
||||
}
|
||||
|
||||
isRequiredApiKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required API key environment variable name for Bedrock.
|
||||
* Bedrock uses AWS credentials, so we return the AWS access key identifier.
|
||||
* @returns {string} The environment variable name
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'AWS_ACCESS_KEY_ID';
|
||||
}
|
||||
|
||||
/**
|
||||
* Override auth validation - Bedrock uses AWS credentials instead of API keys
|
||||
* @param {object} params - Parameters to validate
|
||||
|
||||
@@ -15,6 +15,14 @@ export class ClaudeCodeProvider extends BaseAIProvider {
|
||||
this.name = 'Claude Code';
|
||||
}
|
||||
|
||||
getRequiredApiKeyName() {
|
||||
return 'CLAUDE_CODE_API_KEY';
|
||||
}
|
||||
|
||||
isRequiredApiKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override validateAuth to skip API key validation for Claude Code
|
||||
* @param {object} params - Parameters to validate
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { generateObject, generateText, streamText } from 'ai';
|
||||
import { parse } from 'jsonc-parser';
|
||||
import { BaseAIProvider } from './base-provider.js';
|
||||
import { log } from '../../scripts/modules/index.js';
|
||||
import { log } from '../../scripts/modules/utils.js';
|
||||
|
||||
let createGeminiProvider;
|
||||
|
||||
@@ -652,4 +652,12 @@ Generate ${subtaskCount} subtasks based on the original task context. Return ONL
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getRequiredApiKeyName() {
|
||||
return 'GEMINI_API_KEY';
|
||||
}
|
||||
|
||||
isRequiredApiKey() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,14 @@ export class VertexAIProvider extends BaseAIProvider {
|
||||
this.name = 'Google Vertex AI';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required API key environment variable name for Google Vertex AI.
|
||||
* @returns {string} The environment variable name
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'GOOGLE_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Vertex AI-specific authentication parameters
|
||||
* @param {object} params - Parameters to validate
|
||||
|
||||
@@ -12,6 +12,14 @@ export class GoogleAIProvider extends BaseAIProvider {
|
||||
this.name = 'Google';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the Google API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'GOOGLE_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a Google AI client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
|
||||
@@ -39,4 +39,16 @@ export class OllamaAIProvider extends BaseAIProvider {
|
||||
this.handleError('client initialization', error);
|
||||
}
|
||||
}
|
||||
|
||||
isRequiredApiKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required API key environment variable name for Ollama.
|
||||
* @returns {string} The environment variable name
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'OLLAMA_API_KEY';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ export class OpenAIProvider extends BaseAIProvider {
|
||||
this.name = 'OpenAI';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the OpenAI API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'OPENAI_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns an OpenAI client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
|
||||
@@ -12,6 +12,14 @@ export class OpenRouterAIProvider extends BaseAIProvider {
|
||||
this.name = 'OpenRouter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the OpenRouter API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'OPENROUTER_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns an OpenRouter client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
|
||||
@@ -12,6 +12,14 @@ export class PerplexityAIProvider extends BaseAIProvider {
|
||||
this.name = 'Perplexity';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the Perplexity API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'PERPLEXITY_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a Perplexity client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
|
||||
@@ -12,6 +12,14 @@ export class XAIProvider extends BaseAIProvider {
|
||||
this.name = 'xAI';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable name required for this provider's API key.
|
||||
* @returns {string} The environment variable name for the xAI API key
|
||||
*/
|
||||
getRequiredApiKeyName() {
|
||||
return 'XAI_API_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns an xAI client instance.
|
||||
* @param {object} params - Parameters for client initialization
|
||||
|
||||
@@ -22,6 +22,7 @@ export const CUSTOM_PROVIDERS = {
|
||||
OPENROUTER: 'openrouter',
|
||||
OLLAMA: 'ollama',
|
||||
CLAUDE_CODE: 'claude-code',
|
||||
MCP: 'mcp',
|
||||
GEMINI_CLI: 'gemini-cli'
|
||||
};
|
||||
|
||||
|
||||
134
src/provider-registry/index.js
Normal file
134
src/provider-registry/index.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Provider Registry - Singleton for managing AI providers
|
||||
*
|
||||
* This module implements a singleton registry that allows dynamic registration
|
||||
* of AI providers at runtime, while maintaining compatibility with the existing
|
||||
* static PROVIDERS object in ai-services-unified.js.
|
||||
*/
|
||||
|
||||
// Singleton instance
|
||||
let instance = null;
|
||||
|
||||
/**
|
||||
* Provider Registry class - Manages dynamic provider registration
|
||||
*/
|
||||
class ProviderRegistry {
|
||||
constructor() {
|
||||
// Private provider map
|
||||
this._providers = new Map();
|
||||
|
||||
// Flag to track initialization
|
||||
this._initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance
|
||||
* @returns {ProviderRegistry} The singleton instance
|
||||
*/
|
||||
static getInstance() {
|
||||
if (!instance) {
|
||||
instance = new ProviderRegistry();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the registry
|
||||
* @returns {ProviderRegistry} The singleton instance
|
||||
*/
|
||||
initialize() {
|
||||
if (this._initialized) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a provider with the registry
|
||||
* @param {string} providerName - The name of the provider
|
||||
* @param {object} provider - The provider instance
|
||||
* @param {object} options - Additional options for registration
|
||||
* @returns {ProviderRegistry} The singleton instance for chaining
|
||||
*/
|
||||
registerProvider(providerName, provider, options = {}) {
|
||||
if (!providerName || typeof providerName !== 'string') {
|
||||
throw new Error('Provider name must be a non-empty string');
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('Provider instance is required');
|
||||
}
|
||||
|
||||
// Validate that provider implements the required interface
|
||||
if (
|
||||
typeof provider.generateText !== 'function' ||
|
||||
typeof provider.streamText !== 'function' ||
|
||||
typeof provider.generateObject !== 'function'
|
||||
) {
|
||||
throw new Error('Provider must implement BaseAIProvider interface');
|
||||
}
|
||||
|
||||
// Add provider to the registry
|
||||
this._providers.set(providerName, {
|
||||
instance: provider,
|
||||
options,
|
||||
registeredAt: new Date()
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider exists in the registry
|
||||
* @param {string} providerName - The name of the provider
|
||||
* @returns {boolean} True if the provider exists
|
||||
*/
|
||||
hasProvider(providerName) {
|
||||
return this._providers.has(providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider from the registry
|
||||
* @param {string} providerName - The name of the provider
|
||||
* @returns {object|null} The provider instance or null if not found
|
||||
*/
|
||||
getProvider(providerName) {
|
||||
const providerEntry = this._providers.get(providerName);
|
||||
return providerEntry ? providerEntry.instance : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered providers
|
||||
* @returns {Map} Map of all registered providers
|
||||
*/
|
||||
getAllProviders() {
|
||||
return new Map(this._providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a provider from the registry
|
||||
* @param {string} providerName - The name of the provider
|
||||
* @returns {boolean} True if the provider was removed
|
||||
*/
|
||||
unregisterProvider(providerName) {
|
||||
if (this._providers.has(providerName)) {
|
||||
this._providers.delete(providerName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the registry (primarily for testing)
|
||||
*/
|
||||
reset() {
|
||||
this._providers.clear();
|
||||
this._initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
ProviderRegistry.getInstance().initialize(); // Ensure singleton is initialized on import
|
||||
// Export singleton getter
|
||||
export default ProviderRegistry;
|
||||
@@ -50,7 +50,7 @@ jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
|
||||
}));
|
||||
|
||||
// Mock the log module
|
||||
jest.unstable_mockModule('../../../scripts/modules/index.js', () => ({
|
||||
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
|
||||
log: jest.fn()
|
||||
}));
|
||||
|
||||
@@ -60,7 +60,7 @@ const { GeminiCliProvider } = await import(
|
||||
);
|
||||
const { createGeminiProvider } = await import('ai-sdk-provider-gemini-cli');
|
||||
const { generateObject, generateText, streamText } = await import('ai');
|
||||
const { log } = await import('../../../scripts/modules/index.js');
|
||||
const { log } = await import('../../../scripts/modules/utils.js');
|
||||
|
||||
describe('GeminiCliProvider', () => {
|
||||
let provider;
|
||||
|
||||
103
tests/unit/ai-providers/mcp-components.test.js
Normal file
103
tests/unit/ai-providers/mcp-components.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* tests/unit/ai-providers/mcp-components.test.js
|
||||
* Unit tests for MCP AI SDK custom components
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
describe('MCP Custom SDK Components', () => {
|
||||
describe('Message Converter', () => {
|
||||
let messageConverter;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/message-converter.js'
|
||||
);
|
||||
messageConverter = module;
|
||||
});
|
||||
|
||||
describe('convertToMCPFormat', () => {
|
||||
it('should convert AI SDK messages to MCP format', () => {
|
||||
const input = [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'Hello!' }
|
||||
];
|
||||
|
||||
const result = messageConverter.convertToMCPFormat(input);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messages).toBeDefined();
|
||||
expect(Array.isArray(result.messages)).toBe(true);
|
||||
expect(result.systemPrompt).toBe('You are a helpful assistant.');
|
||||
expect(result.messages).toHaveLength(1);
|
||||
expect(result.messages[0].role).toBe('user');
|
||||
expect(result.messages[0].content.text).toBe('Hello!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertFromMCPFormat', () => {
|
||||
it('should convert MCP response to AI SDK format', () => {
|
||||
const input = {
|
||||
content: 'Hello! How can I help you?',
|
||||
usage: { inputTokens: 10, outputTokens: 8 }
|
||||
};
|
||||
|
||||
const result = messageConverter.convertFromMCPFormat(input);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.text).toBe('Hello! How can I help you?');
|
||||
expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 8 });
|
||||
expect(result.finishReason).toBe('stop');
|
||||
expect(result.warnings).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Language Model', () => {
|
||||
let languageModel;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/language-model.js'
|
||||
);
|
||||
languageModel = module;
|
||||
});
|
||||
|
||||
it('should export MCPLanguageModel class', () => {
|
||||
expect(languageModel.MCPLanguageModel).toBeDefined();
|
||||
expect(typeof languageModel.MCPLanguageModel).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
let errors;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/errors.js'
|
||||
);
|
||||
errors = module;
|
||||
});
|
||||
|
||||
it('should export error classes', () => {
|
||||
expect(errors.MCPError).toBeDefined();
|
||||
expect(typeof errors.MCPError).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Index Module', () => {
|
||||
let index;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/index.js'
|
||||
);
|
||||
index = module;
|
||||
});
|
||||
|
||||
it('should export createMCP function', () => {
|
||||
expect(index.createMCP).toBeDefined();
|
||||
expect(typeof index.createMCP).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -129,25 +129,33 @@ jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
||||
const mockAnthropicProvider = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'ANTHROPIC_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
};
|
||||
|
||||
const mockPerplexityProvider = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'PERPLEXITY_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
};
|
||||
|
||||
const mockOpenAIProvider = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'OPENAI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
};
|
||||
|
||||
const mockOllamaProvider = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => null),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
};
|
||||
|
||||
// Mock the provider classes to return our mock instances
|
||||
@@ -157,44 +165,60 @@ jest.unstable_mockModule('../../src/ai-providers/index.js', () => ({
|
||||
GoogleAIProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'GOOGLE_GENERATIVE_AI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
OpenAIProvider: jest.fn(() => mockOpenAIProvider),
|
||||
XAIProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'XAI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
OpenRouterAIProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'OPENROUTER_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
OllamaAIProvider: jest.fn(() => mockOllamaProvider),
|
||||
BedrockAIProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'AWS_ACCESS_KEY_ID'),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
})),
|
||||
AzureProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'AZURE_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => true)
|
||||
})),
|
||||
VertexAIProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => null),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
})),
|
||||
ClaudeCodeProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'CLAUDE_CODE_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
})),
|
||||
GeminiCliProvider: jest.fn(() => ({
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
generateObject: jest.fn(),
|
||||
getRequiredApiKeyName: jest.fn(() => 'GEMINI_API_KEY'),
|
||||
isRequiredApiKey: jest.fn(() => false)
|
||||
}))
|
||||
}));
|
||||
|
||||
|
||||
103
tests/unit/mcp-providers/mcp-components.test.js
Normal file
103
tests/unit/mcp-providers/mcp-components.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* tests/unit/mcp-providers/mcp-components.test.js
|
||||
* Unit tests for MCP AI SDK custom components
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
describe('MCP Custom SDK Components', () => {
|
||||
describe('Message Converter', () => {
|
||||
let messageConverter;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/message-converter.js'
|
||||
);
|
||||
messageConverter = module;
|
||||
});
|
||||
|
||||
describe('convertToMCPFormat', () => {
|
||||
it('should convert AI SDK messages to MCP format', () => {
|
||||
const input = [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'Hello!' }
|
||||
];
|
||||
|
||||
const result = messageConverter.convertToMCPFormat(input);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.messages).toBeDefined();
|
||||
expect(Array.isArray(result.messages)).toBe(true);
|
||||
expect(result.systemPrompt).toBe('You are a helpful assistant.');
|
||||
expect(result.messages).toHaveLength(1);
|
||||
expect(result.messages[0].role).toBe('user');
|
||||
expect(result.messages[0].content.text).toBe('Hello!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertFromMCPFormat', () => {
|
||||
it('should convert MCP response to AI SDK format', () => {
|
||||
const input = {
|
||||
content: 'Hello! How can I help you?',
|
||||
usage: { inputTokens: 10, outputTokens: 8 }
|
||||
};
|
||||
|
||||
const result = messageConverter.convertFromMCPFormat(input);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.text).toBe('Hello! How can I help you?');
|
||||
expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 8 });
|
||||
expect(result.finishReason).toBe('stop');
|
||||
expect(result.warnings).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Language Model', () => {
|
||||
let languageModel;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/language-model.js'
|
||||
);
|
||||
languageModel = module;
|
||||
});
|
||||
|
||||
it('should export MCPLanguageModel class', () => {
|
||||
expect(languageModel.MCPLanguageModel).toBeDefined();
|
||||
expect(typeof languageModel.MCPLanguageModel).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
let errors;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/errors.js'
|
||||
);
|
||||
errors = module;
|
||||
});
|
||||
|
||||
it('should export error classes', () => {
|
||||
expect(errors.MCPError).toBeDefined();
|
||||
expect(typeof errors.MCPError).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Index Module', () => {
|
||||
let index;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/custom-sdk/index.js'
|
||||
);
|
||||
index = module;
|
||||
});
|
||||
|
||||
it('should export createMCP function', () => {
|
||||
expect(index.createMCP).toBeDefined();
|
||||
expect(typeof index.createMCP).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
107
tests/unit/mcp-providers/mcp-provider.test.js
Normal file
107
tests/unit/mcp-providers/mcp-provider.test.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* tests/unit/mcp-providers/mcp-provider.test.js
|
||||
* Unit tests for MCP provider
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
describe('MCPProvider', () => {
|
||||
let MCPProvider;
|
||||
let provider;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Dynamic import to avoid circular dependency issues
|
||||
const module = await import(
|
||||
'../../../mcp-server/src/providers/mcp-provider.js'
|
||||
);
|
||||
MCPProvider = module.MCPProvider;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new MCPProvider();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with correct name', () => {
|
||||
expect(provider.name).toBe('mcp');
|
||||
});
|
||||
|
||||
it('should initialize with null session', () => {
|
||||
expect(provider.session).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRequiredApiKey', () => {
|
||||
it('should return false (no API key required)', () => {
|
||||
expect(provider.isRequiredApiKey()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAuth', () => {
|
||||
it('should throw error when no session', () => {
|
||||
expect(() => provider.validateAuth({})).toThrow(
|
||||
'MCP Provider requires active MCP session'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when session lacks sampling capabilities', () => {
|
||||
provider.session = {
|
||||
clientCapabilities: {}
|
||||
};
|
||||
|
||||
expect(() => provider.validateAuth({})).toThrow(
|
||||
'MCP session must have client sampling capabilities'
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass validation with valid session', () => {
|
||||
provider.session = {
|
||||
clientCapabilities: {
|
||||
sampling: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(() => provider.validateAuth({})).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSession', () => {
|
||||
it('should set session when provided', () => {
|
||||
const mockSession = {
|
||||
clientCapabilities: { sampling: true }
|
||||
};
|
||||
|
||||
provider.setSession(mockSession);
|
||||
expect(provider.session).toBe(mockSession);
|
||||
});
|
||||
|
||||
it('should handle null session gracefully', () => {
|
||||
provider.setSession(null);
|
||||
expect(provider.session).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasValidSession', () => {
|
||||
it('should return false when no session', () => {
|
||||
expect(provider.hasValidSession()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when session lacks sampling capabilities', () => {
|
||||
provider.session = {
|
||||
clientCapabilities: {}
|
||||
};
|
||||
|
||||
expect(provider.hasValidSession()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true with valid session', () => {
|
||||
provider.session = {
|
||||
clientCapabilities: {
|
||||
sampling: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(provider.hasValidSession()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
417
tests/unit/providers/provider-registry.test.js
Normal file
417
tests/unit/providers/provider-registry.test.js
Normal file
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* Tests for ProviderRegistry - Singleton for managing AI providers
|
||||
*
|
||||
* This test suite covers:
|
||||
* 1. Singleton pattern behavior
|
||||
* 2. Provider registration and validation
|
||||
* 3. Provider retrieval and management
|
||||
* 4. Provider unregistration
|
||||
* 5. Registry reset (for testing)
|
||||
* 6. Interface validation for registered providers
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Import ProviderRegistry
|
||||
const { default: ProviderRegistry } = await import(
|
||||
'../../../src/provider-registry/index.js'
|
||||
);
|
||||
|
||||
// Mock provider classes for testing
|
||||
class MockValidProvider {
|
||||
constructor() {
|
||||
this.name = 'MockValidProvider';
|
||||
}
|
||||
|
||||
generateText() {
|
||||
return Promise.resolve({ text: 'mock text' });
|
||||
}
|
||||
streamText() {
|
||||
return Promise.resolve('mock stream');
|
||||
}
|
||||
generateObject() {
|
||||
return Promise.resolve({ object: {} });
|
||||
}
|
||||
getRequiredApiKeyName() {
|
||||
return 'MOCK_API_KEY';
|
||||
}
|
||||
}
|
||||
|
||||
class MockInvalidProvider {
|
||||
constructor() {
|
||||
this.name = 'MockInvalidProvider';
|
||||
}
|
||||
// Missing required methods: generateText, streamText, generateObject
|
||||
}
|
||||
|
||||
describe('ProviderRegistry', () => {
|
||||
let registry;
|
||||
|
||||
beforeEach(() => {
|
||||
// Get a fresh instance and reset it
|
||||
registry = ProviderRegistry.getInstance();
|
||||
registry.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up after each test
|
||||
registry.reset();
|
||||
});
|
||||
|
||||
describe('Singleton Pattern', () => {
|
||||
test('getInstance returns the same instance', () => {
|
||||
const instance1 = ProviderRegistry.getInstance();
|
||||
const instance2 = ProviderRegistry.getInstance();
|
||||
|
||||
expect(instance1).toBe(instance2);
|
||||
expect(instance1).toBe(registry);
|
||||
});
|
||||
|
||||
test('multiple calls to getInstance return same instance', () => {
|
||||
const instances = Array.from({ length: 5 }, () =>
|
||||
ProviderRegistry.getInstance()
|
||||
);
|
||||
|
||||
instances.forEach((instance) => {
|
||||
expect(instance).toBe(registry);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
test('registry is not auto-initialized when mocked', () => {
|
||||
// When mocked, the auto-initialization at import may not occur
|
||||
expect(registry._initialized).toBe(false);
|
||||
});
|
||||
|
||||
test('initialize sets initialized flag', () => {
|
||||
expect(registry._initialized).toBe(false);
|
||||
|
||||
const result = registry.initialize();
|
||||
|
||||
expect(registry._initialized).toBe(true);
|
||||
expect(result).toBe(registry);
|
||||
});
|
||||
|
||||
test('initialize can be called multiple times safely', () => {
|
||||
// First call initializes
|
||||
registry.initialize();
|
||||
expect(registry._initialized).toBe(true);
|
||||
|
||||
// Second call should not throw
|
||||
expect(() => registry.initialize()).not.toThrow();
|
||||
});
|
||||
|
||||
test('initialize returns self for chaining', () => {
|
||||
const result = registry.initialize();
|
||||
expect(result).toBe(registry);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provider Registration', () => {
|
||||
test('registerProvider adds valid provider successfully', () => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
const options = { priority: 'high' };
|
||||
|
||||
const result = registry.registerProvider('mock', mockProvider, options);
|
||||
|
||||
expect(result).toBe(registry); // Should return self for chaining
|
||||
expect(registry.hasProvider('mock')).toBe(true);
|
||||
});
|
||||
|
||||
test('registerProvider validates provider name', () => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
|
||||
// Test empty string
|
||||
expect(() => registry.registerProvider('', mockProvider)).toThrow(
|
||||
'Provider name must be a non-empty string'
|
||||
);
|
||||
|
||||
// Test null
|
||||
expect(() => registry.registerProvider(null, mockProvider)).toThrow(
|
||||
'Provider name must be a non-empty string'
|
||||
);
|
||||
|
||||
// Test non-string
|
||||
expect(() => registry.registerProvider(123, mockProvider)).toThrow(
|
||||
'Provider name must be a non-empty string'
|
||||
);
|
||||
});
|
||||
|
||||
test('registerProvider validates provider instance', () => {
|
||||
expect(() => registry.registerProvider('mock', null)).toThrow(
|
||||
'Provider instance is required'
|
||||
);
|
||||
|
||||
expect(() => registry.registerProvider('mock', undefined)).toThrow(
|
||||
'Provider instance is required'
|
||||
);
|
||||
});
|
||||
|
||||
test('registerProvider validates provider interface', () => {
|
||||
const invalidProvider = new MockInvalidProvider();
|
||||
|
||||
expect(() => registry.registerProvider('mock', invalidProvider)).toThrow(
|
||||
'Provider must implement BaseAIProvider interface'
|
||||
);
|
||||
});
|
||||
|
||||
test('registerProvider stores provider with metadata', () => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
const options = { priority: 'high', custom: 'value' };
|
||||
const beforeRegistration = new Date();
|
||||
|
||||
registry.registerProvider('mock', mockProvider, options);
|
||||
|
||||
const storedEntry = registry._providers.get('mock');
|
||||
expect(storedEntry.instance).toBe(mockProvider);
|
||||
expect(storedEntry.options).toEqual(options);
|
||||
expect(storedEntry.registeredAt).toBeInstanceOf(Date);
|
||||
expect(storedEntry.registeredAt.getTime()).toBeGreaterThanOrEqual(
|
||||
beforeRegistration.getTime()
|
||||
);
|
||||
});
|
||||
|
||||
test('registerProvider can overwrite existing providers', () => {
|
||||
const provider1 = new MockValidProvider();
|
||||
const provider2 = new MockValidProvider();
|
||||
|
||||
registry.registerProvider('mock', provider1);
|
||||
expect(registry.getProvider('mock')).toBe(provider1);
|
||||
|
||||
registry.registerProvider('mock', provider2);
|
||||
expect(registry.getProvider('mock')).toBe(provider2);
|
||||
});
|
||||
|
||||
test('registerProvider handles missing options', () => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
|
||||
registry.registerProvider('mock', mockProvider);
|
||||
|
||||
const storedEntry = registry._providers.get('mock');
|
||||
expect(storedEntry.options).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provider Retrieval', () => {
|
||||
beforeEach(() => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
registry.registerProvider('mock', mockProvider, { test: 'value' });
|
||||
});
|
||||
|
||||
test('hasProvider returns correct boolean values', () => {
|
||||
expect(registry.hasProvider('mock')).toBe(true);
|
||||
expect(registry.hasProvider('nonexistent')).toBe(false);
|
||||
expect(registry.hasProvider('')).toBe(false);
|
||||
expect(registry.hasProvider(null)).toBe(false);
|
||||
});
|
||||
|
||||
test('getProvider returns correct provider instance', () => {
|
||||
const provider = registry.getProvider('mock');
|
||||
expect(provider).toBeInstanceOf(MockValidProvider);
|
||||
expect(provider.name).toBe('MockValidProvider');
|
||||
});
|
||||
|
||||
test('getProvider returns null for nonexistent provider', () => {
|
||||
expect(registry.getProvider('nonexistent')).toBe(null);
|
||||
expect(registry.getProvider('')).toBe(null);
|
||||
expect(registry.getProvider(null)).toBe(null);
|
||||
});
|
||||
|
||||
test('getAllProviders returns copy of providers map', () => {
|
||||
const mockProvider2 = new MockValidProvider();
|
||||
registry.registerProvider('mock2', mockProvider2);
|
||||
|
||||
const allProviders = registry.getAllProviders();
|
||||
|
||||
expect(allProviders).toBeInstanceOf(Map);
|
||||
expect(allProviders.size).toBe(2);
|
||||
expect(allProviders.has('mock')).toBe(true);
|
||||
expect(allProviders.has('mock2')).toBe(true);
|
||||
|
||||
// Should be a copy, not the original
|
||||
expect(allProviders).not.toBe(registry._providers);
|
||||
});
|
||||
|
||||
test('getAllProviders returns empty map when no providers', () => {
|
||||
registry.reset();
|
||||
|
||||
const allProviders = registry.getAllProviders();
|
||||
|
||||
expect(allProviders).toBeInstanceOf(Map);
|
||||
expect(allProviders.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provider Unregistration', () => {
|
||||
beforeEach(() => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
registry.registerProvider('mock', mockProvider);
|
||||
});
|
||||
|
||||
test('unregisterProvider removes existing provider', () => {
|
||||
expect(registry.hasProvider('mock')).toBe(true);
|
||||
|
||||
const result = registry.unregisterProvider('mock');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(registry.hasProvider('mock')).toBe(false);
|
||||
});
|
||||
|
||||
test('unregisterProvider returns false for nonexistent provider', () => {
|
||||
const result = registry.unregisterProvider('nonexistent');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('unregisterProvider handles edge cases', () => {
|
||||
expect(registry.unregisterProvider('')).toBe(false);
|
||||
expect(registry.unregisterProvider(null)).toBe(false);
|
||||
expect(registry.unregisterProvider(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Registry Reset', () => {
|
||||
beforeEach(() => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
registry.registerProvider('mock', mockProvider);
|
||||
registry.initialize();
|
||||
});
|
||||
|
||||
test('reset clears all providers', () => {
|
||||
expect(registry.hasProvider('mock')).toBe(true);
|
||||
expect(registry._initialized).toBe(true);
|
||||
|
||||
registry.reset();
|
||||
|
||||
expect(registry.hasProvider('mock')).toBe(false);
|
||||
expect(registry._providers.size).toBe(0);
|
||||
});
|
||||
|
||||
test('reset clears initialization flag', () => {
|
||||
expect(registry._initialized).toBe(true);
|
||||
|
||||
registry.reset();
|
||||
|
||||
expect(registry._initialized).toBe(false);
|
||||
});
|
||||
|
||||
// No log assertion for reset, just call reset
|
||||
test('reset can be called without error', () => {
|
||||
expect(() => registry.reset()).not.toThrow();
|
||||
});
|
||||
|
||||
test('reset allows re-initialization', () => {
|
||||
registry.reset();
|
||||
expect(registry._initialized).toBe(false);
|
||||
|
||||
registry.initialize();
|
||||
expect(registry._initialized).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interface Validation', () => {
|
||||
test('validates generateText method exists', () => {
|
||||
const providerWithoutGenerateText = {
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
registry.registerProvider('invalid', providerWithoutGenerateText)
|
||||
).toThrow('Provider must implement BaseAIProvider interface');
|
||||
});
|
||||
|
||||
test('validates streamText method exists', () => {
|
||||
const providerWithoutStreamText = {
|
||||
generateText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
registry.registerProvider('invalid', providerWithoutStreamText)
|
||||
).toThrow('Provider must implement BaseAIProvider interface');
|
||||
});
|
||||
|
||||
test('validates generateObject method exists', () => {
|
||||
const providerWithoutGenerateObject = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn()
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
registry.registerProvider('invalid', providerWithoutGenerateObject)
|
||||
).toThrow('Provider must implement BaseAIProvider interface');
|
||||
});
|
||||
|
||||
test('validates methods are functions', () => {
|
||||
const providerWithNonFunctionMethods = {
|
||||
generateText: 'not a function',
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
registry.registerProvider('invalid', providerWithNonFunctionMethods)
|
||||
).toThrow('Provider must implement BaseAIProvider interface');
|
||||
});
|
||||
|
||||
test('accepts provider with all required methods', () => {
|
||||
const validProvider = {
|
||||
generateText: jest.fn(),
|
||||
streamText: jest.fn(),
|
||||
generateObject: jest.fn()
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
registry.registerProvider('valid', validProvider)
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
test('handles provider registration after reset', () => {
|
||||
const mockProvider = new MockValidProvider();
|
||||
registry.registerProvider('mock', mockProvider);
|
||||
expect(registry.hasProvider('mock')).toBe(true);
|
||||
|
||||
registry.reset();
|
||||
expect(registry.hasProvider('mock')).toBe(false);
|
||||
|
||||
registry.registerProvider('mock', mockProvider);
|
||||
expect(registry.hasProvider('mock')).toBe(true);
|
||||
});
|
||||
|
||||
test('handles multiple registrations and unregistrations', () => {
|
||||
const provider1 = new MockValidProvider();
|
||||
const provider2 = new MockValidProvider();
|
||||
|
||||
registry.registerProvider('provider1', provider1);
|
||||
registry.registerProvider('provider2', provider2);
|
||||
|
||||
expect(registry.getAllProviders().size).toBe(2);
|
||||
|
||||
registry.unregisterProvider('provider1');
|
||||
expect(registry.hasProvider('provider1')).toBe(false);
|
||||
expect(registry.hasProvider('provider2')).toBe(true);
|
||||
|
||||
registry.unregisterProvider('provider2');
|
||||
expect(registry.getAllProviders().size).toBe(0);
|
||||
});
|
||||
|
||||
test('maintains provider isolation', () => {
|
||||
const provider1 = new MockValidProvider();
|
||||
const provider2 = new MockValidProvider();
|
||||
|
||||
registry.registerProvider('provider1', provider1);
|
||||
registry.registerProvider('provider2', provider2);
|
||||
|
||||
const retrieved1 = registry.getProvider('provider1');
|
||||
const retrieved2 = registry.getProvider('provider2');
|
||||
|
||||
expect(retrieved1).toBe(provider1);
|
||||
expect(retrieved2).toBe(provider2);
|
||||
expect(retrieved1).not.toBe(retrieved2);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user