Skip to content

Commit 97d582d

Browse files
committed
Add ask MCP server integration
1 parent 621ce07 commit 97d582d

53 files changed

Lines changed: 3432 additions & 82 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- CreateTable
2+
CREATE TABLE "McpServer" (
3+
"id" TEXT NOT NULL,
4+
"name" TEXT NOT NULL,
5+
"serverUrl" TEXT NOT NULL,
6+
"clientInfo" TEXT,
7+
"orgId" INTEGER NOT NULL,
8+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9+
"updatedAt" TIMESTAMP(3) NOT NULL,
10+
11+
CONSTRAINT "McpServer_pkey" PRIMARY KEY ("id")
12+
);
13+
14+
-- CreateTable
15+
CREATE TABLE "McpServerCredential" (
16+
"id" TEXT NOT NULL,
17+
"userId" TEXT NOT NULL,
18+
"serverId" TEXT NOT NULL,
19+
"tokens" TEXT,
20+
"codeVerifier" TEXT,
21+
"state" TEXT,
22+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
23+
"updatedAt" TIMESTAMP(3) NOT NULL,
24+
25+
CONSTRAINT "McpServerCredential_pkey" PRIMARY KEY ("id")
26+
);
27+
28+
-- CreateIndex
29+
CREATE UNIQUE INDEX "McpServer_serverUrl_orgId_key" ON "McpServer"("serverUrl", "orgId");
30+
31+
-- CreateIndex
32+
CREATE UNIQUE INDEX "McpServerCredential_userId_serverId_key" ON "McpServerCredential"("userId", "serverId");
33+
34+
-- AddForeignKey
35+
ALTER TABLE "McpServer" ADD CONSTRAINT "McpServer_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
36+
37+
-- AddForeignKey
38+
ALTER TABLE "McpServerCredential" ADD CONSTRAINT "McpServerCredential_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
39+
40+
-- AddForeignKey
41+
ALTER TABLE "McpServerCredential" ADD CONSTRAINT "McpServerCredential_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "McpServer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- CreateIndex
2+
CREATE INDEX "McpServerCredential_state_idx" ON "McpServerCredential"("state");
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `name` on the `McpServer` table. All the data in the column will be lost.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "McpServer" DROP COLUMN "name";
9+
10+
-- CreateTable
11+
CREATE TABLE "UserMcpServer" (
12+
"userId" TEXT NOT NULL,
13+
"serverId" TEXT NOT NULL,
14+
"name" TEXT NOT NULL,
15+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
16+
17+
CONSTRAINT "UserMcpServer_pkey" PRIMARY KEY ("userId","serverId")
18+
);
19+
20+
-- AddForeignKey
21+
ALTER TABLE "UserMcpServer" ADD CONSTRAINT "UserMcpServer_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
22+
23+
-- AddForeignKey
24+
ALTER TABLE "UserMcpServer" ADD CONSTRAINT "UserMcpServer_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "McpServer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "McpServerCredential" ADD COLUMN "tokensExpiresAt" TIMESTAMP(3);

packages/db/prisma/schema.prisma

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ model Org {
301301
searchContexts SearchContext[]
302302
303303
chats Chat[]
304+
305+
mcpServers McpServer[]
304306
}
305307

306308
enum OrgRole {
@@ -385,6 +387,9 @@ model User {
385387
oauthAuthCodes OAuthAuthorizationCode[]
386388
oauthRefreshTokens OAuthRefreshToken[]
387389
390+
mcpServerCredentials McpServerCredential[]
391+
userMcpServers UserMcpServer[]
392+
388393
createdAt DateTime @default(now())
389394
updatedAt DateTime @updatedAt
390395
@@ -579,3 +584,72 @@ model OAuthToken {
579584
createdAt DateTime @default(now())
580585
lastUsedAt DateTime?
581586
}
587+
588+
/// An external MCP server endpoint, unique per org.
589+
/// Stores the dynamic client registration (client_id/client_secret) once per org.
590+
model McpServer {
591+
id String @id @default(cuid())
592+
serverUrl String /// MCP server endpoint (e.g., "https://mcp.linear.app/mcp")
593+
594+
/// Dynamic client registration result (RFC 7591).
595+
/// Encrypted JSON of OAuthClientInformation: { client_id, client_secret, client_id_issued_at, client_secret_expires_at }
596+
/// Null until first user in the org triggers registration.
597+
clientInfo String?
598+
599+
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
600+
orgId Int
601+
602+
credentials McpServerCredential[]
603+
userMcpServers UserMcpServer[]
604+
605+
createdAt DateTime @default(now())
606+
updatedAt DateTime @updatedAt
607+
608+
@@unique([serverUrl, orgId])
609+
}
610+
611+
/// Junction table: a user's personal reference to an MCP server with their chosen display name.
612+
model UserMcpServer {
613+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
614+
userId String
615+
616+
server McpServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
617+
serverId String
618+
619+
name String /// User-chosen display name (e.g., "Linear")
620+
621+
createdAt DateTime @default(now())
622+
623+
@@id([userId, serverId])
624+
}
625+
626+
/// Per-user OAuth credentials for an external MCP server.
627+
/// Stores tokens (long-lived) and ephemeral auth-flow state separately.
628+
model McpServerCredential {
629+
id String @id @default(cuid())
630+
631+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
632+
userId String
633+
634+
server McpServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
635+
serverId String
636+
637+
/// OAuth tokens (access_token, refresh_token, etc.) — encrypted JSON of OAuthTokens.
638+
tokens String?
639+
640+
/// Absolute expiry time of the access token, computed at issuance from expires_in.
641+
/// Null when no tokens are stored or the provider did not include expires_in.
642+
tokensExpiresAt DateTime?
643+
644+
/// PKCE code verifier — ephemeral, only used between redirect and callback.
645+
codeVerifier String?
646+
647+
/// OAuth state parameter — ephemeral, for CSRF protection during auth flow.
648+
state String?
649+
650+
createdAt DateTime @default(now())
651+
updatedAt DateTime @updatedAt
652+
653+
@@unique([userId, serverId])
654+
@@index([state])
655+
}

packages/shared/src/env.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ const options = {
239239

240240
SOURCEBOT_CHAT_MODEL_TEMPERATURE: numberSchema.default(0.3),
241241
SOURCEBOT_CHAT_MAX_STEP_COUNT: numberSchema.default(100),
242+
SOURCEBOT_MCP_TOOL_CALL_TIMEOUT_MS: numberSchema.default(60000),
242243

243244
DEBUG_WRITE_CHAT_MESSAGES_TO_FILE: booleanSchema.default('false'),
244245

packages/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@ai-sdk/deepseek": "^2.0.21",
2222
"@ai-sdk/google": "^3.0.34",
2323
"@ai-sdk/google-vertex": "^4.0.68",
24+
"@ai-sdk/mcp": "^2.0.0-beta.11",
2425
"@ai-sdk/mistral": "^3.0.21",
2526
"@ai-sdk/openai": "^3.0.37",
2627
"@ai-sdk/openai-compatible": "^2.0.31",
@@ -193,7 +194,7 @@
193194
"use-stick-to-bottom": "^1.1.3",
194195
"usehooks-ts": "^3.1.0",
195196
"vscode-icons-js": "^11.6.1",
196-
"zod": "^3.25.74",
197+
"zod": "^3.25.76",
197198
"zod-to-json-schema": "^3.24.5"
198199
},
199200
"devDependencies": {

packages/web/src/app/[domain]/askgh/[owner]/[repo]/components/landingPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const LandingPage = ({
7070
<div className="border rounded-md w-full shadow-sm">
7171
<ChatBox
7272
onSubmit={(children) => {
73-
createNewChatThread(children, selectedSearchScopes);
73+
createNewChatThread(children, selectedSearchScopes, []);
7474
}}
7575
className="min-h-[50px]"
7676
isRedirecting={isLoading}

packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ export const ChatThreadPanel = ({
3636
const [inputMessage, setInputMessage] = useState<CreateUIMessage<SBChatMessage> | undefined>(undefined);
3737
const [chatState, setChatState] = useSessionStorage<SetChatStatePayload | null>(SET_CHAT_STATE_SESSION_STORAGE_KEY, null);
3838

39-
// Use the last user's last message to determine what repos and contexts we should select by default.
39+
// Use the last user message to determine what repos, contexts, and MCP state we should select by default.
4040
const lastUserMessage = messages.findLast((message) => message.role === "user");
4141
const defaultSelectedSearchScopes = lastUserMessage?.metadata?.selectedSearchScopes ?? [];
42+
const defaultDisabledMcpServerIds = lastUserMessage?.metadata?.disabledMcpServerIds ?? [];
4243
const [selectedSearchScopes, setSelectedSearchScopes] = useState<SearchScope[]>(defaultSelectedSearchScopes);
43-
44+
const [disabledMcpServerIds, setDisabledMcpServerIds] = useState<string[]>(defaultDisabledMcpServerIds);
45+
4446
useEffect(() => {
4547
if (!chatState) {
4648
return;
@@ -49,6 +51,7 @@ export const ChatThreadPanel = ({
4951
try {
5052
setInputMessage(chatState.inputMessage);
5153
setSelectedSearchScopes(chatState.selectedSearchScopes);
54+
setDisabledMcpServerIds(chatState.disabledMcpServerIds);
5255
} catch {
5356
console.error('Invalid chat state in session storage');
5457
} finally {
@@ -73,6 +76,8 @@ export const ChatThreadPanel = ({
7376
searchContexts={searchContexts}
7477
selectedSearchScopes={selectedSearchScopes}
7578
onSelectedSearchScopesChange={setSelectedSearchScopes}
79+
disabledMcpServerIds={disabledMcpServerIds}
80+
onDisabledMcpServerIdsChange={setDisabledMcpServerIds}
7681
isOwner={isOwner}
7782
isAuthenticated={isAuthenticated}
7883
chatName={chatName}

packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const LandingPageChatBox = ({
2727
}: LandingPageChatBox) => {
2828
const { createNewChatThread, isLoading, loginWall } = useCreateNewChatThread({ isAuthenticated });
2929
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
30+
const [disabledMcpServerIds, setDisabledMcpServerIds] = useLocalStorage<string[]>("disabledMcpServerIds", [], { initializeWithValue: false });
3031
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
3132
const isChatBoxDisabled = languageModels.length === 0;
3233

@@ -35,7 +36,7 @@ export const LandingPageChatBox = ({
3536
<div className="border rounded-md w-full shadow-sm">
3637
<ChatBox
3738
onSubmit={(children) => {
38-
createNewChatThread(children, selectedSearchScopes);
39+
createNewChatThread(children, selectedSearchScopes, disabledMcpServerIds);
3940
}}
4041
className="min-h-[50px]"
4142
isRedirecting={isLoading}
@@ -55,6 +56,8 @@ export const LandingPageChatBox = ({
5556
onSelectedSearchScopesChange={setSelectedSearchScopes}
5657
isContextSelectorOpen={isContextSelectorOpen}
5758
onContextSelectorOpenChanged={setIsContextSelectorOpen}
59+
disabledMcpServerIds={disabledMcpServerIds}
60+
onDisabledMcpServerIdsChange={setDisabledMcpServerIds}
5861
/>
5962
<SearchModeSelector
6063
searchMode="agentic"

0 commit comments

Comments
 (0)