diff --git a/README.md b/README.md index 6f64bac..67e60ad 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ You can deploy your own version of the coding agent template to Vercel with one - **Persistent Storage**: Tasks stored in Neon Postgres database - **Git Integration**: Automatically creates branches and commits changes - **Modern UI**: Clean, responsive interface built with Next.js and Tailwind CSS +- **MCP Server Support**: Connect MCP servers to Claude Code for extended capabilities (Claude only) ## Setup @@ -38,11 +39,7 @@ pnpm install ### 3. Set up environment variables -Copy the example environment file and fill in your values: - -```bash -cp .env.example .env.local -``` +Create a `.env.local` file with your values: Required environment variables: @@ -59,6 +56,7 @@ Optional environment variables: - `CURSOR_API_KEY`: For Cursor agent support - `GEMINI_API_KEY`: For Google Gemini agent support - `NPM_TOKEN`: For private npm packages +- `ENCRYPTION_KEY`: 32-byte hex string for encrypting MCP OAuth secrets (required only when using MCP connectors). Generate with: `openssl rand -hex 32` ### 4. Set up the database @@ -110,6 +108,7 @@ Open [http://localhost:3000](http://localhost:3000) in your browser. - `CURSOR_API_KEY`: Cursor agent API key - `GEMINI_API_KEY`: Google Gemini agent API key (get yours at [Google AI Studio](https://aistudio.google.com/apikey)) - `NPM_TOKEN`: NPM token for private packages +- `ENCRYPTION_KEY`: 32-byte hex string for encrypting MCP OAuth secrets (required only when using MCP connectors). Generate with: `openssl rand -hex 32` ## AI Branch Name Generation @@ -138,6 +137,19 @@ The system automatically generates descriptive Git branch names using AI SDK 5 a - **Sandbox**: [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox) - **Git**: Automated branching and commits with AI-generated branch names +## MCP Server Support + +Connect MCP Servers to extend Claude Code with additional tools and integrations. **Currently only works with Claude Code agent.** + +### How to Add MCP Servers + +1. Go to the "Connectors" tab and click "Add MCP Server" +2. Enter server details (name, base URL, optional OAuth credentials) +3. If using OAuth, generate encryption key: `openssl rand -hex 32` +4. Add to `.env.local`: `ENCRYPTION_KEY=your-32-byte-hex-key` + +**Note**: `ENCRYPTION_KEY` is only required when using MCP servers with OAuth authentication. + ## Development ### Database Operations diff --git a/app/api/connectors/route.ts b/app/api/connectors/route.ts new file mode 100644 index 0000000..bedfe5b --- /dev/null +++ b/app/api/connectors/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db/client' +import { connectors } from '@/lib/db/schema' +import { decrypt } from '@/lib/crypto' + +export async function GET() { + try { + const allConnectors = await db.select().from(connectors) + + const decryptedConnectors = allConnectors.map((connector) => ({ + ...connector, + oauthClientSecret: connector.oauthClientSecret ? decrypt(connector.oauthClientSecret) : null, + })) + + return NextResponse.json({ + success: true, + data: decryptedConnectors, + }) + } catch (error) { + console.error('Error fetching connectors:', error) + + return NextResponse.json( + { + success: false, + error: 'Failed to fetch connectors', + data: [], + }, + { status: 500 }, + ) + } +} diff --git a/app/api/tasks/route.ts b/app/api/tasks/route.ts index 00037d2..a134b1b 100644 --- a/app/api/tasks/route.ts +++ b/app/api/tasks/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse, after } from 'next/server' import { Sandbox } from '@vercel/sandbox' import { db } from '@/lib/db/client' -import { tasks, insertTaskSchema } from '@/lib/db/schema' +import { tasks, insertTaskSchema, connectors } from '@/lib/db/schema' import { generateId } from '@/lib/utils/id' import { createSandbox } from '@/lib/sandbox/creation' import { executeAgentInSandbox, AgentType } from '@/lib/sandbox/agents' @@ -11,6 +11,7 @@ import { eq, desc, or } from 'drizzle-orm' import { createInfoLog } from '@/lib/utils/logging' import { createTaskLogger } from '@/lib/utils/task-logger' import { generateBranchName, createFallbackBranchName } from '@/lib/utils/branch-name-generator' +import { decrypt } from '@/lib/crypto' export async function GET() { try { @@ -358,8 +359,33 @@ async function processTask( throw new Error('Sandbox is not available for agent execution') } + type Connector = typeof connectors.$inferSelect + + let mcpServers: Connector[] = [] + + try { + const allConnectors = await db.select().from(connectors) + mcpServers = allConnectors + .filter((connector: Connector) => connector.status === 'connected') + .map((connector: Connector) => { + return { + ...connector, + oauthClientSecret: connector.oauthClientSecret ? decrypt(connector.oauthClientSecret) : null, + } + }) + + if (mcpServers.length > 0) { + await logger.info( + `Found ${mcpServers.length} connected MCP servers: ${mcpServers.map((s) => s.name).join(', ')}`, + ) + } + } catch (mcpError) { + console.error('Failed to fetch MCP servers:', mcpError) + await logger.info('Warning: Could not fetch MCP servers, continuing without them') + } + const agentResult = await Promise.race([ - executeAgentInSandbox(sandbox, prompt, selectedAgent as AgentType, logger, selectedModel), + executeAgentInSandbox(sandbox, prompt, selectedAgent as AgentType, logger, selectedModel, mcpServers), agentTimeoutPromise, ]) diff --git a/components/app-layout.tsx b/components/app-layout.tsx index 4626225..96f7ce8 100644 --- a/components/app-layout.tsx +++ b/components/app-layout.tsx @@ -2,13 +2,14 @@ import { useState, useEffect, createContext, useContext, useCallback } from 'react' import { TaskSidebar } from '@/components/task-sidebar' -import { Task } from '@/lib/db/schema' +import { Task, Connector } from '@/lib/db/schema' import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Plus } from 'lucide-react' import Link from 'next/link' import { getSidebarWidth, setSidebarWidth, getSidebarOpen, setSidebarOpen } from '@/lib/utils/cookies' import { nanoid } from 'nanoid' +import { ConnectorsProvider } from '@/components/connectors-provider' interface AppLayoutProps { children: React.ReactNode @@ -264,72 +265,74 @@ export function AppLayout({ children, initialSidebarWidth, initialSidebarOpen }: return ( -
- {/* Backdrop - Mobile Only */} - {isSidebarOpen &&
} - - {/* Sidebar */} +
+ {/* Backdrop - Mobile Only */} + {isSidebarOpen &&
} + + {/* Sidebar */} +
-
- {isLoading ? ( - - ) : ( - - )} +
+ {isLoading ? ( + + ) : ( + + )} +
-
- {/* Resize Handle - Desktop Only, when sidebar is open */} -