Skip to content

Commit 063cb6a

Browse files
fveiraswwwKinfe123
andauthored
feat(connectors): add MCP server support (claude only) (#10)
* feat(connectors): add MCP server support (claude only) * fix: revert configFileCmd * Update components/connectors/manage-connectors.tsx Co-authored-by: KinfeMichael Tariku <[email protected]> * feat: make ENCRYPTION_KEY optional for MCP features --------- Co-authored-by: Francisco Veiras <[email protected]> Co-authored-by: KinfeMichael Tariku <[email protected]>
1 parent 82af231 commit 063cb6a

File tree

20 files changed

+3833
-3622
lines changed

20 files changed

+3833
-3622
lines changed

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ You can deploy your own version of the coding agent template to Vercel with one
2020
- **Persistent Storage**: Tasks stored in Neon Postgres database
2121
- **Git Integration**: Automatically creates branches and commits changes
2222
- **Modern UI**: Clean, responsive interface built with Next.js and Tailwind CSS
23+
- **MCP Server Support**: Connect MCP servers to Claude Code for extended capabilities (Claude only)
2324

2425
## Setup
2526

@@ -38,11 +39,7 @@ pnpm install
3839

3940
### 3. Set up environment variables
4041

41-
Copy the example environment file and fill in your values:
42-
43-
```bash
44-
cp .env.example .env.local
45-
```
42+
Create a `.env.local` file with your values:
4643

4744
Required environment variables:
4845

@@ -59,6 +56,7 @@ Optional environment variables:
5956
- `CURSOR_API_KEY`: For Cursor agent support
6057
- `GEMINI_API_KEY`: For Google Gemini agent support
6158
- `NPM_TOKEN`: For private npm packages
59+
- `ENCRYPTION_KEY`: 32-byte hex string for encrypting MCP OAuth secrets (required only when using MCP connectors). Generate with: `openssl rand -hex 32`
6260

6361
### 4. Set up the database
6462

@@ -110,6 +108,7 @@ Open [http://localhost:3000](http://localhost:3000) in your browser.
110108
- `CURSOR_API_KEY`: Cursor agent API key
111109
- `GEMINI_API_KEY`: Google Gemini agent API key (get yours at [Google AI Studio](https://aistudio.google.com/apikey))
112110
- `NPM_TOKEN`: NPM token for private packages
111+
- `ENCRYPTION_KEY`: 32-byte hex string for encrypting MCP OAuth secrets (required only when using MCP connectors). Generate with: `openssl rand -hex 32`
113112

114113
## AI Branch Name Generation
115114

@@ -138,6 +137,19 @@ The system automatically generates descriptive Git branch names using AI SDK 5 a
138137
- **Sandbox**: [Vercel Sandbox](https://vercel.com/docs/vercel-sandbox)
139138
- **Git**: Automated branching and commits with AI-generated branch names
140139

140+
## MCP Server Support
141+
142+
Connect MCP Servers to extend Claude Code with additional tools and integrations. **Currently only works with Claude Code agent.**
143+
144+
### How to Add MCP Servers
145+
146+
1. Go to the "Connectors" tab and click "Add MCP Server"
147+
2. Enter server details (name, base URL, optional OAuth credentials)
148+
3. If using OAuth, generate encryption key: `openssl rand -hex 32`
149+
4. Add to `.env.local`: `ENCRYPTION_KEY=your-32-byte-hex-key`
150+
151+
**Note**: `ENCRYPTION_KEY` is only required when using MCP servers with OAuth authentication.
152+
141153
## Development
142154

143155
### Database Operations

app/api/connectors/route.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from 'next/server'
2+
import { db } from '@/lib/db/client'
3+
import { connectors } from '@/lib/db/schema'
4+
import { decrypt } from '@/lib/crypto'
5+
6+
export async function GET() {
7+
try {
8+
const allConnectors = await db.select().from(connectors)
9+
10+
const decryptedConnectors = allConnectors.map((connector) => ({
11+
...connector,
12+
oauthClientSecret: connector.oauthClientSecret ? decrypt(connector.oauthClientSecret) : null,
13+
}))
14+
15+
return NextResponse.json({
16+
success: true,
17+
data: decryptedConnectors,
18+
})
19+
} catch (error) {
20+
console.error('Error fetching connectors:', error)
21+
22+
return NextResponse.json(
23+
{
24+
success: false,
25+
error: 'Failed to fetch connectors',
26+
data: [],
27+
},
28+
{ status: 500 },
29+
)
30+
}
31+
}

app/api/tasks/route.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NextRequest, NextResponse, after } from 'next/server'
22
import { Sandbox } from '@vercel/sandbox'
33
import { db } from '@/lib/db/client'
4-
import { tasks, insertTaskSchema } from '@/lib/db/schema'
4+
import { tasks, insertTaskSchema, connectors } from '@/lib/db/schema'
55
import { generateId } from '@/lib/utils/id'
66
import { createSandbox } from '@/lib/sandbox/creation'
77
import { executeAgentInSandbox, AgentType } from '@/lib/sandbox/agents'
@@ -11,6 +11,7 @@ import { eq, desc, or } from 'drizzle-orm'
1111
import { createInfoLog } from '@/lib/utils/logging'
1212
import { createTaskLogger } from '@/lib/utils/task-logger'
1313
import { generateBranchName, createFallbackBranchName } from '@/lib/utils/branch-name-generator'
14+
import { decrypt } from '@/lib/crypto'
1415

1516
export async function GET() {
1617
try {
@@ -358,8 +359,33 @@ async function processTask(
358359
throw new Error('Sandbox is not available for agent execution')
359360
}
360361

362+
type Connector = typeof connectors.$inferSelect
363+
364+
let mcpServers: Connector[] = []
365+
366+
try {
367+
const allConnectors = await db.select().from(connectors)
368+
mcpServers = allConnectors
369+
.filter((connector: Connector) => connector.status === 'connected')
370+
.map((connector: Connector) => {
371+
return {
372+
...connector,
373+
oauthClientSecret: connector.oauthClientSecret ? decrypt(connector.oauthClientSecret) : null,
374+
}
375+
})
376+
377+
if (mcpServers.length > 0) {
378+
await logger.info(
379+
`Found ${mcpServers.length} connected MCP servers: ${mcpServers.map((s) => s.name).join(', ')}`,
380+
)
381+
}
382+
} catch (mcpError) {
383+
console.error('Failed to fetch MCP servers:', mcpError)
384+
await logger.info('Warning: Could not fetch MCP servers, continuing without them')
385+
}
386+
361387
const agentResult = await Promise.race([
362-
executeAgentInSandbox(sandbox, prompt, selectedAgent as AgentType, logger, selectedModel),
388+
executeAgentInSandbox(sandbox, prompt, selectedAgent as AgentType, logger, selectedModel, mcpServers),
363389
agentTimeoutPromise,
364390
])
365391

components/app-layout.tsx

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import { useState, useEffect, createContext, useContext, useCallback } from 'react'
44
import { TaskSidebar } from '@/components/task-sidebar'
5-
import { Task } from '@/lib/db/schema'
5+
import { Task, Connector } from '@/lib/db/schema'
66
import { useRouter } from 'next/navigation'
77
import { Button } from '@/components/ui/button'
88
import { Plus } from 'lucide-react'
99
import Link from 'next/link'
1010
import { getSidebarWidth, setSidebarWidth, getSidebarOpen, setSidebarOpen } from '@/lib/utils/cookies'
1111
import { nanoid } from 'nanoid'
12+
import { ConnectorsProvider } from '@/components/connectors-provider'
1213

1314
interface AppLayoutProps {
1415
children: React.ReactNode
@@ -264,72 +265,74 @@ export function AppLayout({ children, initialSidebarWidth, initialSidebarOpen }:
264265

265266
return (
266267
<TasksContext.Provider value={{ refreshTasks: fetchTasks, toggleSidebar, isSidebarOpen, addTaskOptimistically }}>
267-
<div
268-
className="h-screen flex relative"
269-
style={
270-
{
271-
'--sidebar-width': `${sidebarWidth}px`,
272-
'--sidebar-open': isSidebarOpen ? '1' : '0',
273-
} as React.CSSProperties
274-
}
275-
suppressHydrationWarning
276-
>
277-
{/* Backdrop - Mobile Only */}
278-
{isSidebarOpen && <div className="lg:hidden fixed inset-0 bg-black/50 z-30" onClick={closeSidebar} />}
279-
280-
{/* Sidebar */}
268+
<ConnectorsProvider>
281269
<div
282-
className={`
270+
className="h-screen flex relative"
271+
style={
272+
{
273+
'--sidebar-width': `${sidebarWidth}px`,
274+
'--sidebar-open': isSidebarOpen ? '1' : '0',
275+
} as React.CSSProperties
276+
}
277+
suppressHydrationWarning
278+
>
279+
{/* Backdrop - Mobile Only */}
280+
{isSidebarOpen && <div className="lg:hidden fixed inset-0 bg-black/50 z-30" onClick={closeSidebar} />}
281+
282+
{/* Sidebar */}
283+
<div
284+
className={`
283285
fixed inset-y-0 left-0 z-40
284286
${isResizing ? '' : 'transition-all duration-300 ease-in-out'}
285287
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
286288
${isSidebarOpen ? 'pointer-events-auto' : 'pointer-events-none'}
287289
`}
288-
style={{
289-
width: `${sidebarWidth}px`,
290-
}}
291-
>
292-
<div
293-
className="h-full overflow-hidden"
294290
style={{
295291
width: `${sidebarWidth}px`,
296292
}}
297293
>
298-
{isLoading ? (
299-
<SidebarLoader width={sidebarWidth} />
300-
) : (
301-
<TaskSidebar tasks={tasks} onTaskSelect={handleTaskSelect} width={sidebarWidth} />
302-
)}
294+
<div
295+
className="h-full overflow-hidden"
296+
style={{
297+
width: `${sidebarWidth}px`,
298+
}}
299+
>
300+
{isLoading ? (
301+
<SidebarLoader width={sidebarWidth} />
302+
) : (
303+
<TaskSidebar tasks={tasks} onTaskSelect={handleTaskSelect} width={sidebarWidth} />
304+
)}
305+
</div>
303306
</div>
304-
</div>
305307

306-
{/* Resize Handle - Desktop Only, when sidebar is open */}
307-
<div
308-
className={`
308+
{/* Resize Handle - Desktop Only, when sidebar is open */}
309+
<div
310+
className={`
309311
hidden lg:block fixed inset-y-0 cursor-col-resize group z-41 hover:bg-primary/20
310312
${isResizing ? '' : 'transition-all duration-300 ease-in-out'}
311313
${isSidebarOpen ? 'w-1 opacity-100' : 'w-0 opacity-0'}
312314
`}
313-
onMouseDown={isSidebarOpen ? handleMouseDown : undefined}
314-
style={{
315-
// Position it right after the sidebar
316-
left: isSidebarOpen ? `${sidebarWidth}px` : '0px',
317-
}}
318-
>
319-
<div className="absolute inset-0 w-2 -ml-0.5" />
320-
<div className="absolute inset-y-0 left-0 w-0.5 bg-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" />
321-
</div>
315+
onMouseDown={isSidebarOpen ? handleMouseDown : undefined}
316+
style={{
317+
// Position it right after the sidebar
318+
left: isSidebarOpen ? `${sidebarWidth}px` : '0px',
319+
}}
320+
>
321+
<div className="absolute inset-0 w-2 -ml-0.5" />
322+
<div className="absolute inset-y-0 left-0 w-0.5 bg-primary/50 opacity-0 group-hover:opacity-100 transition-opacity" />
323+
</div>
322324

323-
{/* Main Content */}
324-
<div
325-
className={`flex-1 overflow-auto flex flex-col lg:ml-0 ${isResizing ? '' : 'transition-all duration-300 ease-in-out'}`}
326-
style={{
327-
marginLeft: isSidebarOpen ? `${sidebarWidth + 4}px` : '0px',
328-
}}
329-
>
330-
{children}
325+
{/* Main Content */}
326+
<div
327+
className={`flex-1 overflow-auto flex flex-col lg:ml-0 ${isResizing ? '' : 'transition-all duration-300 ease-in-out'}`}
328+
style={{
329+
marginLeft: isSidebarOpen ? `${sidebarWidth + 4}px` : '0px',
330+
}}
331+
>
332+
{children}
333+
</div>
331334
</div>
332-
</div>
335+
</ConnectorsProvider>
333336
</TasksContext.Provider>
334337
)
335338
}

components/connectors-provider.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client'
2+
3+
import { useState, useEffect, createContext, useContext, useCallback } from 'react'
4+
import { Connector } from '@/lib/db/schema'
5+
6+
interface ConnectorsContextType {
7+
connectors: Connector[]
8+
refreshConnectors: () => Promise<void>
9+
isLoading: boolean
10+
}
11+
12+
const ConnectorsContext = createContext<ConnectorsContextType | undefined>(undefined)
13+
14+
export const useConnectors = () => {
15+
const context = useContext(ConnectorsContext)
16+
if (!context) {
17+
throw new Error('useConnectors must be used within ConnectorsProvider')
18+
}
19+
return context
20+
}
21+
22+
interface ConnectorsProviderProps {
23+
children: React.ReactNode
24+
}
25+
26+
export function ConnectorsProvider({ children }: ConnectorsProviderProps) {
27+
const [connectors, setConnectors] = useState<Connector[]>([])
28+
const [isLoading, setIsLoading] = useState(true)
29+
30+
const fetchConnectors = useCallback(async () => {
31+
try {
32+
const response = await fetch('/api/connectors')
33+
if (response.ok) {
34+
const data = await response.json()
35+
setConnectors(data.data || [])
36+
}
37+
} catch (error) {
38+
console.error('Error fetching connectors:', error)
39+
} finally {
40+
setIsLoading(false)
41+
}
42+
}, [])
43+
44+
useEffect(() => {
45+
fetchConnectors()
46+
}, [fetchConnectors])
47+
48+
const refreshConnectors = useCallback(async () => {
49+
await fetchConnectors()
50+
}, [fetchConnectors])
51+
52+
return (
53+
<ConnectorsContext.Provider
54+
value={{
55+
connectors,
56+
refreshConnectors,
57+
isLoading,
58+
}}
59+
>
60+
{children}
61+
</ConnectorsContext.Provider>
62+
)
63+
}

0 commit comments

Comments
 (0)