Skip to content

Commit 23fde36

Browse files
dcramercodex
andcommitted
feat(mcp): Add plugin-scoped MCP tool support
Add plugin manifest support for HTTP MCP servers and a turn-local MCP tool manager that progressively exposes plugin tools when matching skills load. Pause turns on MCP auth challenges and resume the same session after the callback instead of failing the turn. Persist loaded skills and active MCP providers across resume, and align the resumed callback path with normal reply delivery and thread-state persistence so Slack state stays consistent. Co-Authored-By: Codex <noreply@openai.com>
1 parent 5f51079 commit 23fde36

26 files changed

Lines changed: 3577 additions & 109 deletions

packages/junior/evals/behavior-harness.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export async function runBehaviorEvalCase(
423423
...(mockImageGeneration
424424
? {
425425
toolOverrides: {
426-
...context.toolOverrides,
426+
...(context?.toolOverrides ?? {}),
427427
imageGenerate: createMockImageGenerateDeps(),
428428
},
429429
}

packages/junior/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@chat-adapter/state-redis": "4.20.2",
4343
"@mariozechner/pi-agent-core": "^0.56.3",
4444
"@mariozechner/pi-ai": "^0.56.3",
45+
"@modelcontextprotocol/sdk": "1.27.1",
4546
"@sinclair/typebox": "^0.34.48",
4647
"@slack/web-api": "^7.14.1",
4748
"@vercel/queue": "^0.1.3",
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import type {
2+
OAuthClientInformationMixed,
3+
OAuthTokens,
4+
} from "@modelcontextprotocol/sdk/shared/auth.js";
5+
import type { OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
6+
import type { ThreadArtifactsState } from "@/chat/slack-actions/types";
7+
import { getStateAdapter } from "@/chat/state";
8+
9+
const MCP_AUTH_SESSION_PREFIX = "junior:mcp_auth_session";
10+
const MCP_AUTH_CREDENTIALS_PREFIX = "junior:mcp_auth_credentials";
11+
const MCP_AUTH_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
12+
const MCP_AUTH_CREDENTIALS_TTL_MS = 30 * 24 * 60 * 60 * 1000;
13+
14+
export interface McpAuthSessionState {
15+
authSessionId: string;
16+
provider: string;
17+
userId: string;
18+
conversationId: string;
19+
sessionId: string;
20+
userMessage: string;
21+
channelId?: string;
22+
threadTs?: string;
23+
toolChannelId?: string;
24+
configuration?: Record<string, unknown>;
25+
artifactState?: ThreadArtifactsState;
26+
authorizationUrl?: string;
27+
codeVerifier?: string;
28+
createdAtMs: number;
29+
updatedAtMs: number;
30+
}
31+
32+
export interface McpStoredOAuthCredentials {
33+
clientInformation?: OAuthClientInformationMixed;
34+
discoveryState?: OAuthDiscoveryState;
35+
tokens?: OAuthTokens;
36+
}
37+
38+
function sessionKey(authSessionId: string): string {
39+
return `${MCP_AUTH_SESSION_PREFIX}:${authSessionId}`;
40+
}
41+
42+
function credentialsKey(userId: string, provider: string): string {
43+
return `${MCP_AUTH_CREDENTIALS_PREFIX}:${userId}:${provider}`;
44+
}
45+
46+
function isRecord(value: unknown): value is Record<string, unknown> {
47+
return typeof value === "object" && value !== null;
48+
}
49+
50+
function parseMcpAuthSession(value: unknown): McpAuthSessionState | undefined {
51+
if (typeof value !== "string") {
52+
return undefined;
53+
}
54+
55+
try {
56+
const parsed = JSON.parse(value) as Record<string, unknown>;
57+
if (!isRecord(parsed)) {
58+
return undefined;
59+
}
60+
61+
if (
62+
typeof parsed.authSessionId !== "string" ||
63+
typeof parsed.provider !== "string" ||
64+
typeof parsed.userId !== "string" ||
65+
typeof parsed.conversationId !== "string" ||
66+
typeof parsed.sessionId !== "string" ||
67+
typeof parsed.userMessage !== "string" ||
68+
typeof parsed.createdAtMs !== "number" ||
69+
typeof parsed.updatedAtMs !== "number"
70+
) {
71+
return undefined;
72+
}
73+
74+
return {
75+
authSessionId: parsed.authSessionId,
76+
provider: parsed.provider,
77+
userId: parsed.userId,
78+
conversationId: parsed.conversationId,
79+
sessionId: parsed.sessionId,
80+
userMessage: parsed.userMessage,
81+
createdAtMs: parsed.createdAtMs,
82+
updatedAtMs: parsed.updatedAtMs,
83+
...(typeof parsed.channelId === "string"
84+
? { channelId: parsed.channelId }
85+
: {}),
86+
...(typeof parsed.threadTs === "string"
87+
? { threadTs: parsed.threadTs }
88+
: {}),
89+
...(typeof parsed.toolChannelId === "string"
90+
? { toolChannelId: parsed.toolChannelId }
91+
: {}),
92+
...(isRecord(parsed.configuration)
93+
? { configuration: parsed.configuration }
94+
: {}),
95+
...(isRecord(parsed.artifactState)
96+
? { artifactState: parsed.artifactState as ThreadArtifactsState }
97+
: {}),
98+
...(typeof parsed.authorizationUrl === "string"
99+
? { authorizationUrl: parsed.authorizationUrl }
100+
: {}),
101+
...(typeof parsed.codeVerifier === "string"
102+
? { codeVerifier: parsed.codeVerifier }
103+
: {}),
104+
};
105+
} catch {
106+
return undefined;
107+
}
108+
}
109+
110+
function parseStoredCredentials(
111+
value: unknown,
112+
): McpStoredOAuthCredentials | undefined {
113+
if (typeof value !== "string") {
114+
return undefined;
115+
}
116+
117+
try {
118+
const parsed = JSON.parse(value) as Record<string, unknown>;
119+
if (!isRecord(parsed)) {
120+
return undefined;
121+
}
122+
123+
return {
124+
...(isRecord(parsed.clientInformation)
125+
? {
126+
clientInformation:
127+
parsed.clientInformation as OAuthClientInformationMixed,
128+
}
129+
: {}),
130+
...(isRecord(parsed.discoveryState)
131+
? {
132+
discoveryState:
133+
parsed.discoveryState as unknown as OAuthDiscoveryState,
134+
}
135+
: {}),
136+
...(isRecord(parsed.tokens)
137+
? { tokens: parsed.tokens as OAuthTokens }
138+
: {}),
139+
};
140+
} catch {
141+
return undefined;
142+
}
143+
}
144+
145+
export async function getMcpAuthSession(
146+
authSessionId: string,
147+
): Promise<McpAuthSessionState | undefined> {
148+
await getStateAdapter().connect();
149+
return parseMcpAuthSession(
150+
await getStateAdapter().get(sessionKey(authSessionId)),
151+
);
152+
}
153+
154+
export async function putMcpAuthSession(
155+
session: McpAuthSessionState,
156+
ttlMs: number = MCP_AUTH_SESSION_TTL_MS,
157+
): Promise<void> {
158+
await getStateAdapter().connect();
159+
await getStateAdapter().set(
160+
sessionKey(session.authSessionId),
161+
JSON.stringify(session),
162+
ttlMs,
163+
);
164+
}
165+
166+
export async function patchMcpAuthSession(
167+
authSessionId: string,
168+
patch: Partial<McpAuthSessionState>,
169+
): Promise<McpAuthSessionState> {
170+
const current = await getMcpAuthSession(authSessionId);
171+
if (!current) {
172+
throw new Error(`Unknown MCP auth session: ${authSessionId}`);
173+
}
174+
175+
const next: McpAuthSessionState = {
176+
...current,
177+
...patch,
178+
authSessionId: current.authSessionId,
179+
provider: current.provider,
180+
userId: current.userId,
181+
conversationId: current.conversationId,
182+
sessionId: current.sessionId,
183+
userMessage: current.userMessage,
184+
createdAtMs: current.createdAtMs,
185+
updatedAtMs: Date.now(),
186+
};
187+
await putMcpAuthSession(next);
188+
return next;
189+
}
190+
191+
export async function deleteMcpAuthSession(
192+
authSessionId: string,
193+
): Promise<void> {
194+
await getStateAdapter().connect();
195+
await getStateAdapter().delete(sessionKey(authSessionId));
196+
}
197+
198+
export async function getMcpStoredOAuthCredentials(
199+
userId: string,
200+
provider: string,
201+
): Promise<McpStoredOAuthCredentials | undefined> {
202+
await getStateAdapter().connect();
203+
return parseStoredCredentials(
204+
await getStateAdapter().get(credentialsKey(userId, provider)),
205+
);
206+
}
207+
208+
export async function putMcpStoredOAuthCredentials(
209+
userId: string,
210+
provider: string,
211+
value: McpStoredOAuthCredentials,
212+
ttlMs: number = MCP_AUTH_CREDENTIALS_TTL_MS,
213+
): Promise<void> {
214+
await getStateAdapter().connect();
215+
await getStateAdapter().set(
216+
credentialsKey(userId, provider),
217+
JSON.stringify(value),
218+
ttlMs,
219+
);
220+
}

0 commit comments

Comments
 (0)