Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions mcp-server/src/providers/mcp-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Follows the Claude Code provider pattern for session-based providers.
*/

import { createMCP } from '../custom-sdk/index.js';
import { createMCPSampling } from '@tm/ai-sdk-provider-mcp-sampling';
import { BaseAIProvider } from '../../../src/ai-providers/base-provider.js';

export class MCPProvider extends BaseAIProvider {
Expand Down Expand Up @@ -47,8 +47,8 @@ export class MCPProvider extends BaseAIProvider {
*/
getClient(params) {
try {
// Pass MCP session to AI SDK implementation
return createMCP({
// Pass MCP session to AI SDK v5 implementation
return createMCPSampling({
session: this.session,
defaultSettings: {
temperature: params.temperature,
Expand Down
13 changes: 13 additions & 0 deletions packages/ai-sdk-provider-mcp-sampling/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# @tm/ai-sdk-provider-mcp-sampling

## 0.1.0

### Minor Changes

- Initial release of MCP Sampling AI SDK provider
- Support for AI SDK v5 with v2 specification
- Full MCP sampling integration
- TypeScript support
- Streaming support (simulated)
- Structured output support
- Comprehensive error handling
64 changes: 64 additions & 0 deletions packages/ai-sdk-provider-mcp-sampling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# MCP Sampling AI SDK Provider

AI SDK v5 provider for MCP (Model Context Protocol) Sampling integration with Task Master.

## Overview

This package provides an AI SDK v5 compatible provider for using MCP sampling capabilities within Task Master. It implements the v2 specification required by AI SDK v5.

## Usage

```typescript
import { createMCPSampling } from '@tm/ai-sdk-provider-mcp-sampling';

// Create provider with MCP session
const mcpProvider = createMCPSampling({
session: mcpSession, // Your MCP session object
defaultSettings: {
temperature: 0.7,
maxTokens: 1000
}
});

// Use with AI SDK
const model = mcpProvider('claude-3-5-sonnet-20241022');
const result = await generateText({
model,
prompt: 'Hello, world!'
});
```

## Features

- AI SDK v5 compatible with v2 specification
- Full support for MCP sampling protocol
- TypeScript support with comprehensive types
- Streaming support (simulated)
- Structured output support via JSON extraction
- Comprehensive error handling
- Proper usage tracking

## Requirements

- Node.js >= 20
- AI SDK v5
- Active MCP session with sampling capabilities

## Architecture

This provider follows the same patterns as other Task Master AI SDK providers:

- `MCPSamplingLanguageModel` - Main language model implementation
- `createMCPSampling` - Provider factory function
- Message conversion between AI SDK and MCP formats
- Error handling and mapping to AI SDK error types
- JSON extraction for structured outputs

## Error Handling

The provider maps MCP errors to appropriate AI SDK error types:

- Session errors → `MCPSamplingError`
- Authentication errors → `LoadAPIKeyError`
- API errors → `APICallError`
- Model not found → `NoSuchModelError`
33 changes: 33 additions & 0 deletions packages/ai-sdk-provider-mcp-sampling/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@tm/ai-sdk-provider-mcp-sampling",
"private": true,
"description": "AI SDK provider for MCP Sampling integration",
"type": "module",
"types": "./src/index.ts",
"main": "./dist/index.js",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.10",
"jsonc-parser": "^3.3.1"
},
"devDependencies": {
"@types/node": "^22.18.6",
"typescript": "^5.9.2",
"vitest": "^4.0.10"
},
"engines": {
"node": ">=20"
},
"keywords": ["ai", "mcp", "sampling", "language-model", "provider"],
"files": ["dist/**/*", "README.md"],
"version": ""
}
103 changes: 103 additions & 0 deletions packages/ai-sdk-provider-mcp-sampling/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Error classes and utilities for MCP Sampling provider
*/

import {
APICallError,
LoadAPIKeyError,
NoSuchModelError
} from '@ai-sdk/provider';

export interface MCPSamplingErrorOptions {
message?: string;
cause?: unknown;
session?: unknown;
responseData?: unknown;
isRetryable?: boolean;
}

export class MCPSamplingError extends Error {
constructor(message: string, public readonly options: MCPSamplingErrorOptions = {}) {
super(message);
this.name = 'MCPSamplingError';
}
}

export function createMCPAPICallError(
options: MCPSamplingErrorOptions & {
statusCode?: number;
responseHeaders?: Record<string, string>;
}
): APICallError {
return new APICallError({
message: options.message || 'MCP API call failed',
cause: options.cause,
data: options.responseData,
isRetryable: options.isRetryable ?? false,
responseHeaders: options.responseHeaders,
statusCode: options.statusCode
});
}

export function createMCPAuthenticationError(
options: MCPSamplingErrorOptions = {}
): LoadAPIKeyError {
return new LoadAPIKeyError({
message: options.message || 'MCP session authentication failed'
});
}

export function createMCPSessionError(
options: MCPSamplingErrorOptions = {}
): MCPSamplingError {
return new MCPSamplingError(
options.message || 'MCP session error',
options
);
}

export function mapMCPError(error: unknown): Error {
if (error instanceof MCPSamplingError) {
return error;
}

if (error instanceof Error) {
// Map common MCP errors to appropriate AI SDK errors
if (error.message.includes('unauthorized') ||
error.message.includes('authentication')) {
return createMCPAuthenticationError({
message: `MCP authentication failed: ${error.message}`,
cause: error
});
}

if (error.message.includes('timeout') ||
error.message.includes('timed out')) {
return createMCPAPICallError({
message: `MCP request timed out: ${error.message}`,
cause: error,
isRetryable: true
});
}

if (error.message.includes('model') &&
error.message.includes('not found')) {
return new NoSuchModelError({
modelId: 'unknown',
modelType: 'languageModel'
});
}

return createMCPAPICallError({
message: `MCP API error: ${error.message}`,
cause: error,
isRetryable: false
});
}

return createMCPAPICallError({
message: 'Unknown MCP error occurred',
cause: error,
isRetryable: false
});
}
32 changes: 32 additions & 0 deletions packages/ai-sdk-provider-mcp-sampling/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* MCP Sampling Provider for AI SDK v5
*/

export { createMCPSampling } from './mcp-sampling-provider.js';
export { MCPSamplingLanguageModel } from './mcp-sampling-language-model.js';

// Export types
export type {
MCPSamplingModelId,
MCPSamplingSettings,
MCPSamplingLanguageModelOptions,
MCPSession,
MCPSamplingResponse
} from './types.js';

// Export error utilities
export {
MCPSamplingError,
createMCPAPICallError,
createMCPAuthenticationError,
createMCPSessionError,
mapMCPError
} from './errors.js';

// Export utility functions
export { extractJson } from './json-extractor.js';
export {
convertToMCPFormat,
convertFromMCPFormat,
createPromptFromMessages
} from './message-converter.js';
56 changes: 56 additions & 0 deletions packages/ai-sdk-provider-mcp-sampling/src/json-extractor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* JSON extraction utilities for MCP Sampling provider
*/

/**
* Extract JSON from text response
* Handles various formats including code blocks and plain JSON
*/
export function extractJson(text: string): string {
if (!text || typeof text !== 'string') {
throw new Error('Input text is empty or not a string');
}

const trimmedText = text.trim();

// Try to find JSON in code blocks first
const codeBlockMatch = trimmedText.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (codeBlockMatch) {
return codeBlockMatch[1].trim();
}

// Try to find JSON between specific markers
const markerMatch = trimmedText.match(/```json\s*([\s\S]*?)\s*```/i);
if (markerMatch) {
return markerMatch[1].trim();
}

// Look for JSON object/array patterns
const jsonObjectMatch = trimmedText.match(/\{[\s\S]*\}/);
const jsonArrayMatch = trimmedText.match(/\[[\s\S]*\]/);

if (jsonObjectMatch && jsonArrayMatch) {
// Return the first match that appears
const objectIndex = trimmedText.indexOf(jsonObjectMatch[0]);
const arrayIndex = trimmedText.indexOf(jsonArrayMatch[0]);
return objectIndex < arrayIndex ? jsonObjectMatch[0] : jsonArrayMatch[0];
}

if (jsonObjectMatch) {
return jsonObjectMatch[0];
}

if (jsonArrayMatch) {
return jsonArrayMatch[0];
}

// If nothing found, try to parse the entire text as JSON
try {
JSON.parse(trimmedText);
return trimmedText;
} catch {
// If all else fails, return the original text
// The caller should handle JSON parsing errors
return trimmedText;
}
}
Loading