Skip to content

Commit 987c442

Browse files
authored
feat: implement cold start-like auto connection for MCP server and simplify status (#73)
The MCP server now connects automatically on first use and disconnects after a period of inactivity. When you call a tool, the server will automatically reconnect, so no manual action is needed. This change ensures that tool calls work smoothly even in deployment environments like Vercel.
1 parent a379cd3 commit 987c442

9 files changed

Lines changed: 74 additions & 115 deletions

File tree

.github/.dangerfile.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
11
// @ts-nocheck
2-
import { danger, fail, warn } from "danger";
2+
import { danger, fail, warn, message } from "danger";
33

44
const prTitle = danger.github.pr.title;
55
const conventionalRegex =
6-
/^(feat|fix|chore|docs|style|refactor|test|perf|build)(\(.+\))?!?: .+/;
6+
/^(feat|fix|chore|docs|style|refactor|test|perf|build|ci|revert)(\(.+\))?!?: .+/;
77

8-
if (!conventionalRegex.test(prTitle))
8+
if (!conventionalRegex.test(prTitle)) {
99
fail(
1010
`❌ The PR title does not follow the Conventional Commit format.
11-
12-
Expected formats include:
13-
- feat: add login functionality
14-
- fix: correct redirect bug
15-
- chore: update dependency xyz
16-
17-
Supported prefixes:
18-
- feat
19-
- fix
20-
- chore
21-
- docs
22-
- style
23-
- refactor
24-
- test
25-
- perf
26-
- build
27-
28-
Please update your PR title to match one of these formats.`,
11+
12+
**Current title:** "${prTitle}"
13+
14+
**Expected formats:**
15+
- \`feat: add login functionality\`
16+
- \`fix: correct redirect bug\`
17+
- \`chore: update dependency xyz\`
18+
- \`feat(auth): add OAuth integration\`
19+
20+
**Supported prefixes:**
21+
- \`feat\` - new features
22+
- \`fix\` - bug fixes
23+
- \`chore\` - maintenance tasks
24+
- \`docs\` - documentation changes
25+
- \`style\` - formatting changes
26+
- \`refactor\` - code refactoring
27+
- \`test\` - test additions/changes
28+
- \`perf\` - performance improvements
29+
- \`build\` - build system changes
30+
- \`ci\` - CI configuration changes
31+
- \`revert\` - reverting changes
32+
33+
Please update your PR title to match one of these formats.`,
2934
);
35+
} else {
36+
message("✅ PR title follows Conventional Commit format!");
37+
}
38+
39+
if (prTitle.length > 100) {
40+
warn("⚠️ PR title is quite long. Consider keeping it under 100 characters.");
41+
}
42+
43+
if (prTitle.length < 10) {
44+
warn("⚠️ PR title seems too short. Consider being more descriptive.");
45+
}

.github/workflows/pr-check.yml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
name: PR Title Check
2-
3-
on:
4-
pull_request:
5-
types: [opened, synchronize, reopened, edited]
62
permissions:
7-
contents: write
3+
contents: read
84
pull-requests: write
95
issues: write
6+
statuses: write
7+
checks: write
8+
on:
9+
pull_request:
10+
types: [opened, synchronize, reopened, edited]
1011

1112
jobs:
1213
danger:
@@ -21,10 +22,7 @@ jobs:
2122
with:
2223
node-version: 20
2324

24-
- name: Install Danger globally
25-
run: npm install -g danger
26-
2725
- name: Run Danger
28-
run: danger ci --dangerfile=./.github/.dangerfile.ts
26+
run: npx danger ci --dangerfile=./.github/.dangerfile.ts
2927
env:
3028
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

src/app/api/mcp/actions.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -84,30 +84,6 @@ export async function removeMcpClientAction(name: string) {
8484
await mcpClientsManager.removeClient(name);
8585
}
8686

87-
export async function connectMcpClientAction(name: string) {
88-
const client = await mcpClientsManager
89-
.getClients()
90-
.then((clients) =>
91-
clients.find((client) => client.getInfo().name === name),
92-
);
93-
if (client?.getInfo().status === "connected") {
94-
return;
95-
}
96-
await client?.connect();
97-
}
98-
99-
export async function disconnectMcpClientAction(name: string) {
100-
const client = await mcpClientsManager
101-
.getClients()
102-
.then((clients) =>
103-
clients.find((client) => client.getInfo().name === name),
104-
);
105-
if (client?.getInfo().status === "disconnected") {
106-
return;
107-
}
108-
await client?.disconnect();
109-
}
110-
11187
export async function refreshMcpClientAction(name: string) {
11288
await mcpClientsManager.refreshClient(name);
11389
}

src/components/enabled-mcp-tools.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export function EnabledMcpTools({
4343

4444
const enabledMcpTools = useMemo(() => {
4545
const mcpTools = mcpList
46-
.filter((mcp) => mcp.status == "connected")
4746
.map((mcp) => {
4847
const serverName = mcp.name;
4948
const allowedMcpServerTools = allowedMcpServers?.[serverName]?.tools;

src/components/mcp-card.tsx

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Alert, AlertDescription, AlertTitle } from "ui/alert";
1313
import { Button } from "ui/button";
1414
import { Card, CardContent, CardHeader } from "ui/card";
1515
import JsonView from "ui/json-view";
16-
import { Separator } from "ui/separator";
1716
import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip";
1817
import { memo, useCallback, useMemo, useState } from "react";
1918
import Link from "next/link";
@@ -22,14 +21,11 @@ import { safe } from "ts-safe";
2221

2322
import { handleErrorWithToast } from "ui/shared-toast";
2423
import {
25-
connectMcpClientAction,
26-
disconnectMcpClientAction,
2724
refreshMcpClientAction,
2825
removeMcpClientAction,
2926
} from "@/app/api/mcp/actions";
3027
import type { MCPServerInfo, MCPToolInfo } from "app-types/mcp";
31-
import { Switch } from "ui/switch";
32-
import { Label } from "ui/label";
28+
3329
import { ToolDetailPopup } from "./tool-detail-popup";
3430
import { useTranslations } from "next-intl";
3531

@@ -109,14 +105,6 @@ export const MCPCard = memo(function MCPCard({
109105
await pipeProcessing(() => removeMcpClientAction(name));
110106
}, [name]);
111107

112-
const handleToggleConnection = useCallback(async () => {
113-
await pipeProcessing(() =>
114-
status === "connected"
115-
? disconnectMcpClientAction(name)
116-
: connectMcpClientAction(name),
117-
);
118-
}, [name, status]);
119-
120108
return (
121109
<Card className="relative hover:border-foreground/20 transition-colors bg-secondary/40">
122110
{isLoading && (
@@ -127,22 +115,6 @@ export const MCPCard = memo(function MCPCard({
127115

128116
<h4 className="font-bold text-xs sm:text-lg">{name}</h4>
129117
<div className="flex-1" />
130-
131-
<Label
132-
htmlFor={`mcp-card-switch-${name}`}
133-
className="mr-2 text-xs text-muted-foreground"
134-
>
135-
{status === "connected" ? "enabled" : "disabled"}
136-
</Label>
137-
<Switch
138-
id={`mcp-card-switch-${name}`}
139-
checked={status === "connected"}
140-
onCheckedChange={handleToggleConnection}
141-
className="mr-2 hidden sm:block"
142-
/>
143-
<div className="h-4 hidden sm:block">
144-
<Separator orientation="vertical" />
145-
</div>
146118
<Tooltip>
147119
<TooltipTrigger asChild>
148120
<Link

src/components/prompt-input.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,23 +90,21 @@ export default function PromptInput({
9090

9191
const toolList = useMemo(() => {
9292
return (
93-
mcpList
94-
?.filter((mcp) => mcp.status === "connected")
95-
.flatMap((mcp) => [
96-
{
97-
id: mcp.name,
98-
label: mcp.name,
99-
type: "server",
100-
},
101-
...mcp.toolInfo.map((tool) => {
102-
const id = createMCPToolId(mcp.name, tool.name);
103-
return {
104-
id,
105-
label: id,
106-
type: "tool",
107-
};
108-
}),
109-
]) ?? []
93+
mcpList?.flatMap((mcp) => [
94+
{
95+
id: mcp.name,
96+
label: mcp.name,
97+
type: "server",
98+
},
99+
...mcp.toolInfo.map((tool) => {
100+
const id = createMCPToolId(mcp.name, tool.name);
101+
return {
102+
id,
103+
label: id,
104+
type: "tool",
105+
};
106+
}),
107+
]) ?? []
110108
);
111109
}, [mcpList]);
112110

src/components/tool-selector.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,7 @@ function McpServerSelector() {
359359
selectedMcpServerList.map((server) => (
360360
<DropdownMenuSub key={server.id}>
361361
<DropdownMenuSubTrigger
362-
className={cn(
363-
server.status === "disconnected" && "opacity-40",
364-
"flex items-center gap-2 font-semibold cursor-pointer",
365-
)}
362+
className="flex items-center gap-2 font-semibold cursor-pointer"
366363
icon={
367364
<div className="flex items-center gap-2 ml-auto">
368365
{server.tools.filter((t) => t.checked).length > 0 ? (
@@ -395,14 +392,6 @@ function McpServerSelector() {
395392
>
396393
error
397394
</span>
398-
) : server.status === "disconnected" ? (
399-
<span
400-
className={cn(
401-
"text-xs text-muted-foreground ml-1 p-1 rounded",
402-
)}
403-
>
404-
disabled
405-
</span>
406395
) : null}
407396
</DropdownMenuSubTrigger>
408397
<DropdownMenuPortal>

src/lib/ai/mcp/create-mcp-client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ export class MCPClient {
203203
}
204204
async callTool(toolName: string, input?: unknown) {
205205
return safe(() => this.log.info("tool call", toolName))
206+
207+
.ifOk(() => {
208+
if (this.error) {
209+
throw new Error(
210+
"MCP Server is currently in an error state. Please check the configuration and try refreshing the server.",
211+
);
212+
}
213+
})
214+
.ifOk(() => this.scheduleAutoDisconnect()) // disconnect if autoDisconnectSeconds is set
206215
.map(async () => {
207216
const client = await this.connect();
208217
return client?.callTool({

src/lib/ai/mcp/create-mcp-clients-manager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class MCPClientsManager {
5555
tools() {
5656
return Object.fromEntries(
5757
Array.from(this.clients.values())
58-
.filter((client) => client.getInfo().status === "connected")
58+
.filter((client) => client.getInfo().toolInfo.length > 0)
5959
.flatMap((client) =>
6060
Object.entries(client.tools).map(([name, tool]) => [
6161
createMCPToolId(client.getInfo().name, name),
@@ -79,7 +79,9 @@ export class MCPClientsManager {
7979
void prevClient.disconnect();
8080
}
8181

82-
const client = createMCPClient(name, serverConfig);
82+
const client = createMCPClient(name, serverConfig, {
83+
autoDisconnectSeconds: 60 * 30, // 30 minutes
84+
});
8385
this.clients.set(name, client);
8486
return client.connect();
8587
}

0 commit comments

Comments
 (0)