Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/supercode-cli/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supercode-cli",
"version": "0.1.61",
"version": "0.1.62",
"description": "AI-powered coding agent CLI",
"main": "dist/main.js",
"bin": {
Expand Down
9 changes: 9 additions & 0 deletions apps/supercode-cli/server/src/cli/ai/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,15 @@ async function streamAIResponse(
delete (toolsToUse as Record<string, unknown>).firecrawl_scrape
delete (toolsToUse as Record<string, unknown>).firecrawl_map
}
// If composio MCP tools are available, instruct the AI to prefer them
// over built-in tools for the corresponding services.
const hasComposioTools = Object.keys(toolsToUse).some((k) =>
k.startsWith("mcp_composio_")
)
if (hasComposioTools && aiMessages[0]) {
aiMessages[0].content += `\n\n## MCP Tool Preference\n\nYou have composio-connected MCP tools available (prefixed with mcp_composio_). These provide direct access to services like GitHub, Linear, Slack, etc. When a user's request can be satisfied using these MCP tools, prefer them over running commands via run_command or other built-in tools. For example, use mcp_composio_github_* tools for GitHub operations instead of running gh CLI commands.`
}

// Wire the subagent runtime so the `delegate` tool can spawn focused subtasks.
setDelegateRuntime({
model: (provider as any).model ?? null,
Expand Down
186 changes: 87 additions & 99 deletions apps/supercode-cli/server/src/cli/commands/slashCommands/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as readline from "node:readline"
import { select, text, password, isCancel } from "@clack/prompts"
import { select, text, isCancel } from "@clack/prompts"
import chalk from "chalk"
let _mgr: any = null
async function mcpManager(): Promise<any> {
Expand Down Expand Up @@ -70,6 +70,23 @@ class McpPicker {
detail: "API key set — connect to start",
})
}
} else {
// No local API key — try server-side proxy
try {
const apps = await composioSessionManager.listAppsFromServer()
for (const app of apps) {
list.push({
id: `app:${app.slug}`,
name: app.name,
type: "composio-app",
status: app.connected ? "connected" : "disconnected",
detail: app.connected ? "OAuth connected" : "click to connect with OAuth",
appSlug: app.slug,
})
}
} catch {
// server-side also unavailable — no composio apps shown
}
}

for (const [name, srv] of Object.entries(servers)) {
Expand Down Expand Up @@ -320,7 +337,6 @@ async function connectFlow(): Promise<void> {
const mgr = await mcpManager()

// Try server-side session first (no local API key needed)
let sessionErr: Error | null = null
try {
const info = await composioSessionManager.createSessionFromServer()
const config = await getCliConfig()
Expand All @@ -334,46 +350,11 @@ async function connectFlow(): Promise<void> {
console.log(`\n ${chalk.hex(theme.green)("◆")} composio connected — ${Object.keys(tools).length} tools`)
return
} catch (err: any) {
sessionErr = err
}

// Fall back to local API key
if (!composioSessionManager.isConfigured) {
const apiKey = (await password({
message: "Composio API key (set COMPOSIO_API_KEY in .env)",
})) as string
if (isCancel(apiKey)) return

if (apiKey.trim()) {
process.env.COMPOSIO_API_KEY = apiKey.trim()
const { ComposioSessionManager } = await import("src/mcp/composio")
const config = await getCliConfig()
const existing = (config as Record<string, any>) ?? {}
await saveCliConfig({
...existing as any,
composioApiKey: apiKey.trim(),
} as any)
Object.assign(composioSessionManager, new ComposioSessionManager())
console.log(`\n ${chalk.hex(theme.red)("◆")} composio connection failed: ${err.message}`)
if (!composioSessionManager.isConfigured) {
console.log(` ${chalk.hex(theme.muted)(" ")}Set COMPOSIO_API_KEY in .env for local development, or run ${chalk.hex(theme.green)("supercode login")}`)
}
}

try {
const info = await composioSessionManager.createSession("supercode-cli")
const config = await getCliConfig()
const existing = (config as Record<string, any>) ?? {}
await saveCliConfig({
...existing as any,
composioSessionId: info.sessionId,
} as any)
await mgr.reconnectServer("composio", { url: info.url, headers: info.headers })
const tools = await mgr.getTools("composio")
console.log(`\n ${chalk.hex(theme.green)("◆")} composio connected — ${Object.keys(tools).length} tools`)
} catch (err: any) {
const msg = !composioSessionManager.isConfigured
? "Server unreachable and no local API key set"
: err.message
console.log(`\n ${chalk.hex(theme.red)("◆")} composio connection failed: ${msg}`)
}
return
}

Expand Down Expand Up @@ -589,79 +570,86 @@ async function showInteractiveList(): Promise<void> {
const wasRaw = process.stdin.isRaw
if (process.stdin.isTTY) process.stdin.setRawMode(true)

while (true) {
const key = await readRawKey()

if (key === "escape") {
clearLines(picker.overlayLines + 1)
break
}

if (key === "up") {
picker.selectPrev()
clearLines(picker.overlayLines)
draw()
continue
}

if (key === "down") {
picker.selectNext()
clearLines(picker.overlayLines)
draw()
continue
}

if (key === "enter") {
clearLines(picker.overlayLines + 1)
try {
while (true) {
const key = await readRawKey()

if (picker.selected === -1) {
await connectFlow()
} else {
const entry = picker.getSelectedEntry()
if (entry && entry.type === "composio-app" && entry.status === "disconnected" && entry.appSlug) {
await oauthConnectAppFlow(entry.appSlug)
} else if (entry) {
await showDetail(entry)
}
if (key === "escape") {
break
}

await picker.refresh()
if (key === "up") {
picker.selectPrev()
clearLines(picker.overlayLines)
draw()
continue
}

if (process.stdin.isTTY) process.stdin.setRawMode(true)
process.stdout.write("\n")
draw()
continue
}
if (key === "down") {
picker.selectNext()
clearLines(picker.overlayLines)
draw()
continue
}

if (key === "space") {
const entry = picker.getSelectedEntry()
if (entry) {
const mgr = await mcpManager()
const connected = mgr.connectedServers
if (key === "enter") {
clearLines(picker.overlayLines + 1)

if (connected.includes(entry.name)) {
await mgr.stopServer(entry.name)
if (entry.name === "composio") {
composioSessionManager.resetSession()
}
} else {
const cfg = await buildServerConfig(entry)
if (cfg) {
try {
await mgr.reconnectServer(entry.name, cfg)
} catch {}
try {
if (picker.selected === -1) {
await connectFlow()
} else {
const entry = picker.getSelectedEntry()
if (entry && entry.type === "composio-app" && entry.status === "disconnected" && entry.appSlug) {
await oauthConnectAppFlow(entry.appSlug)
} else if (entry) {
await showDetail(entry)
}
}
} catch {
// @clack/prompts may leave stdin paused; ensure it's flowing
}

await picker.refresh()
clearLines(picker.overlayLines)

if (process.stdin.isTTY) process.stdin.setRawMode(true)
process.stdout.write("\n")
draw()
continue
}

if (key === "space") {
const entry = picker.getSelectedEntry()
if (entry) {
const mgr = await mcpManager()
const connected = mgr.connectedServers

if (connected.includes(entry.name)) {
await mgr.stopServer(entry.name)
if (entry.name === "composio") {
composioSessionManager.resetSession()
}
} else {
const cfg = await buildServerConfig(entry)
if (cfg) {
try {
await mgr.reconnectServer(entry.name, cfg)
} catch {}
}
}

await picker.refresh()
clearLines(picker.overlayLines)
draw()
}
continue
}
continue
}
} finally {
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false)
process.stdin.resume()
clearLines(picker.overlayLines + 1)
}

if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false)
}

async function oauthConnectAppFlow(slug: string): Promise<void> {
Expand Down
61 changes: 61 additions & 0 deletions apps/supercode-cli/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1103,12 +1103,73 @@ app.post("/api/composio/session", async (req, res) => {
url: (s as any).mcp.url as string,
headers: (s as any).mcp.headers as Record<string, string>,
sessionId: (s as any).session_id as string,
apiKey,
})
} catch (error: any) {
res.status(500).json({ error: error.message || "Composio session creation failed" })
}
})

app.post("/api/composio/apps", async (req, res) => {
try {
const user = await getUserFromBearer(req)
if (!user) { res.status(401).json({ error: "Unauthorized" }); return }

const apiKey = process.env.COMPOSIO_API_KEY
if (!apiKey) { res.status(500).json({ error: "Composio not configured on server" }); return }

const { Composio } = await import("@composio/core")
const composio = new Composio({ apiKey })

const [authConfigs, toolkits, connectedRes] = await Promise.all([
(composio as any).authConfigs.list({}),
(composio.toolkits as any).get(),
(composio.connectedAccounts as any).list({}),
])

const configuredSlugs = new Set<string>(
(authConfigs.items ?? []).map((ac: any) => ac.toolkit?.slug).filter(Boolean),
)

const connectedMap = new Map<string, string>()
for (const acct of connectedRes.items ?? []) {
const slug: string = acct.toolkit?.slug
if (slug && acct.status === "ACTIVE") {
connectedMap.set(slug, acct.id)
}
}

const toolkitMap = new Map<string, any>()
for (const tk of toolkits) {
toolkitMap.set(tk.slug, tk)
}

const apps: any[] = []
for (const slug of configuredSlugs) {
const tk = toolkitMap.get(slug)
if (!tk) continue
const conn = connectedMap.get(slug)
apps.push({
slug: tk.slug,
name: tk.name,
description: tk.meta?.description ?? "",
logo: tk.meta?.logo,
connected: !!conn,
connectedAccountId: conn ?? null,
})
}

apps.sort((a, b) => {
if (a.connected !== b.connected) return a.connected ? -1 : 1
return a.name.localeCompare(b.name)
})

res.json({ apps })
} catch (error: any) {
res.status(500).json({ error: error.message || "Composio list apps failed" })
}
})

app.post("/api/tools/web-search", async (req, res) => {
try {
const user = await getUserFromBearer(req)
Expand Down
45 changes: 45 additions & 0 deletions apps/supercode-cli/server/src/mcp/composio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface ComposioSessionInfo {
url: string
headers: Record<string, string>
sessionId: string
apiKey?: string
}

export interface AppEntry {
Expand Down Expand Up @@ -78,6 +79,15 @@ export class ComposioSessionManager {

const info = (await res.json()) as ComposioSessionInfo
this.session = info

if (info.apiKey && !this.composio) {
try {
this.composio = new Composio({ apiKey: info.apiKey })
} catch {
// SDK init from server key failed — listApps won't work locally
}
}

return info
}

Expand Down Expand Up @@ -115,6 +125,41 @@ export class ComposioSessionManager {
return result
}

async listAppsFromServer(
serverUrl = BASE_URL,
accessToken?: string,
): Promise<AppEntry[]> {
if (!accessToken) {
const stored = await getStoredToken()
accessToken = stored?.access_token as string
}
if (!accessToken) {
throw new Error("Not authenticated — run supercode login first")
}

const res = await fetch(`${serverUrl}/api/composio/apps`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new Error((body as any).error || `Server returned ${res.status}`)
}

const data = (await res.json()) as { apps: any[] }
return data.apps.map((a) => ({
slug: a.slug,
name: a.name,
description: a.description,
logo: a.logo,
connected: a.connected,
connectedAccountId: a.connectedAccountId,
}))
}

resetSession(): void {
this.session = null
}
Expand Down
Loading