Skip to content
Draft
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
50 changes: 25 additions & 25 deletions packages/junior/src/chat/mcp/tool-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,6 @@ export interface ManagedMcpToolDescriptor {
provider: string;
}

type ActiveMcpSkillScope = Pick<SkillMetadata, "pluginProvider">;

type ActiveMcpSkill = Pick<SkillMetadata, "name" | "pluginProvider">;

export interface ManagedMcpTool extends ManagedMcpToolDescriptor {
Expand Down Expand Up @@ -200,6 +198,27 @@ export class McpToolManager {
);
}

/** Return all configured MCP providers with active/inactive state.
* Never connects to any MCP server. */
getAvailableProviderCatalog(): Array<{
provider: string;
description: string;
active: boolean;
}> {
return [...this.pluginsByProvider.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([provider, plugin]) => ({
provider,
description: plugin.manifest.description,
active: this.activeProviders.has(provider),
}));
}

/** Return true when the provider is configured (active or not). */
hasConfiguredProvider(provider: string): boolean {
return this.pluginsByProvider.has(provider);
}

async activateForSkill(skill: ActiveMcpSkill): Promise<boolean> {
if (!skill.pluginProvider) {
return false;
Expand Down Expand Up @@ -262,11 +281,11 @@ export class McpToolManager {
}
}

/** Return descriptors for all active MCP provider tools, optionally filtered by provider. */
getActiveToolCatalog(
skills: ActiveMcpSkillScope[],
options: { provider?: string } = {},
): ManagedMcpToolDescriptor[] {
return this.getResolvedActiveTools(skills, options).map((tool) =>
return this.getResolvedActiveTools(options).map((tool) =>
this.toToolDescriptor(tool),
);
}
Expand Down Expand Up @@ -415,9 +434,8 @@ export class McpToolManager {
return true;
}

/** Return all active ManagedMcpTool objects for the given skill scope. */
/** Return all active ManagedMcpTool objects, optionally filtered by provider. */
getResolvedActiveTools(
skills: ActiveMcpSkillScope[],
options: { provider?: string } = {},
): ManagedMcpTool[] {
const resolved: ManagedMcpTool[] = [];
Expand All @@ -427,30 +445,12 @@ export class McpToolManager {
continue;
}

resolved.push(...this.resolveProviderTools(provider, skills));
resolved.push(...(this.toolsByProvider.get(provider) ?? []));
}

return resolved;
}

private resolveProviderTools(
provider: string,
skills: ActiveMcpSkillScope[],
): ManagedMcpTool[] {
const providerTools = this.toolsByProvider.get(provider) ?? [];
if (providerTools.length === 0) {
return [];
}

const relevantSkills = skills.filter(
(skill) => skill.pluginProvider === provider,
);
if (relevantSkills.length === 0) {
return [];
}
return providerTools;
}

private toToolDescriptor(tool: ManagedMcpTool): ManagedMcpToolDescriptor {
return {
name: tool.name,
Expand Down
34 changes: 33 additions & 1 deletion packages/junior/src/chat/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
} from "@/chat/sandbox/paths";
import type { ThreadArtifactsState } from "@/chat/state/artifacts";
import type { Skill, SkillMetadata, SkillInvocation } from "@/chat/skills";
import type { ActiveMcpCatalogSummary } from "@/chat/tools/skill/mcp-tool-summary";
import type {
ActiveMcpCatalogSummary,
AvailableMcpProviderSummary,
} from "@/chat/tools/skill/mcp-tool-summary";
import { escapeXml } from "@/chat/xml";

const DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
Expand Down Expand Up @@ -252,6 +255,25 @@ function formatActiveMcpCatalogsForPrompt(
return lines.join("\n");
}

function formatAvailableMcpProvidersForPrompt(
providers: AvailableMcpProviderSummary[],
): string | null {
const inactive = providers.filter((p) => !p.active);
if (inactive.length === 0) {
return null;
}
const lines = [
"These MCP providers are configured but not yet connected. Supply the provider name to `searchMcpTools` to connect and list its tools.",
];
for (const p of inactive) {
lines.push(" <provider>");
lines.push(` <name>${escapeXml(p.provider)}</name>`);
lines.push(` <description>${escapeXml(p.description)}</description>`);
lines.push(" </provider>");
}
return lines.join("\n");
}

interface ToolPromptContext {
name: string;
promptGuidelines?: string[];
Expand Down Expand Up @@ -538,6 +560,7 @@ function buildCapabilitiesSection(params: {
availableSkills: SkillMetadata[];
activeSkills: Skill[];
activeMcpCatalogs: ActiveMcpCatalogSummary[];
availableMcpProviders: AvailableMcpProviderSummary[];
invocation: SkillInvocation | null;
toolGuidance?: ToolPromptContext[];
}): string | null {
Expand All @@ -562,6 +585,13 @@ function buildCapabilitiesSection(params: {
blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
}

const availableProviders = formatAvailableMcpProvidersForPrompt(
params.availableMcpProviders,
);
if (availableProviders) {
blocks.push(renderTagBlock("available-mcp-providers", availableProviders));
}

const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
if (toolGuidance) {
blocks.push(renderTagBlock("tool-guidance", toolGuidance));
Expand All @@ -578,6 +608,7 @@ type TurnContextPromptInput = {
availableSkills: SkillMetadata[];
activeSkills: Skill[];
activeMcpCatalogs?: ActiveMcpCatalogSummary[];
availableMcpProviders?: AvailableMcpProviderSummary[];
toolGuidance?: ToolPromptContext[];
runtime?: {
conversationId?: string;
Expand Down Expand Up @@ -628,6 +659,7 @@ export function buildTurnContextPrompt(params: TurnContextPromptInput): string {
availableSkills: params.availableSkills,
activeSkills: params.activeSkills,
activeMcpCatalogs: params.activeMcpCatalogs ?? [],
availableMcpProviders: params.availableMcpProviders ?? [],
invocation: params.invocation,
toolGuidance: params.toolGuidance ?? [],
}),
Expand Down
37 changes: 28 additions & 9 deletions packages/junior/src/chat/respond.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ export async function generateAssistantReply(
let lastKnownSandboxDependencyProfileHash: string | undefined =
context.sandbox?.sandboxDependencyProfileHash;
let loadedSkillNamesForResume: string[] = [];
let activeMcpProviderNamesForResume: string[] = [];
let mcpToolManager: McpToolManager | undefined;
let sandboxExecutor: SandboxExecutor | undefined;
let timedOut = false;
Expand Down Expand Up @@ -682,6 +683,8 @@ export async function generateAssistantReply(
pluginAuth.getPendingPause() ?? mcpAuth.getPendingPause();
const syncResumeState = () => {
loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
activeMcpProviderNamesForResume =
turnMcpToolManager.getActiveProviders();
};
setTags({
conversationId: spanContext.conversationId,
Expand Down Expand Up @@ -747,12 +750,9 @@ export async function generateAssistantReply(
) {
return undefined;
}
const availableToolCount = turnMcpToolManager.getActiveToolCatalog(
activeSkills,
{
provider: effective.pluginProvider,
},
).length;
const availableToolCount = turnMcpToolManager.getActiveToolCatalog({
provider: effective.pluginProvider,
}).length;
return {
mcp_provider: effective.pluginProvider,
available_tool_count: availableToolCount,
Expand All @@ -769,8 +769,8 @@ export async function generateAssistantReply(
userText: userInput,
artifactState: context.artifactState,
configuration: configurationValues,
getActiveSkills: () => activeSkills,
mcpToolManager: turnMcpToolManager,
onProviderActivated: syncResumeState,
sandbox,
advisor: {
config: botConfig.advisor,
Expand All @@ -790,7 +790,18 @@ export async function generateAssistantReply(
promptSnippet: definition.promptSnippet,
}));

syncResumeState();
// ── MCP provider activation ──────────────────────────────────────
// Restore previously activated MCP providers from the checkpoint so
// providers connected in a prior turn are available immediately.
for (const provider of existingCheckpoint?.activeMcpProviderNames ?? []) {
await turnMcpToolManager.activateProvider(provider);
syncResumeState();
if (mcpAuth.getPendingPause()) {
timeoutResumeMessages = existingCheckpoint?.piMessages ?? [];
throw mcpAuth.getPendingPause()!;
}
}
// Activate MCP for checkpoint-preloaded skills (backward compat with loadSkill).
for (const skill of activeSkills) {
await turnMcpToolManager.activateForSkill(skill);
syncResumeState();
Expand All @@ -803,13 +814,16 @@ export async function generateAssistantReply(

// ── Prompt context ───────────────────────────────────────────────
const activeMcpCatalogs = toActiveMcpCatalogSummaries(
turnMcpToolManager.getActiveToolCatalog(activeSkills),
turnMcpToolManager.getActiveToolCatalog(),
);
const availableMcpProviders =
turnMcpToolManager.getAvailableProviderCatalog();
baseInstructions = buildSystemPrompt();
const turnContextPrompt = buildTurnContextPrompt({
availableSkills,
activeSkills,
activeMcpCatalogs,
availableMcpProviders,
toolGuidance,
runtime: {
conversationId: spanContext.conversationId,
Expand Down Expand Up @@ -912,6 +926,7 @@ export async function generateAssistantReply(
sliceId: currentSliceId,
messages,
loadedSkillNames: loadedSkillNamesForResume,
activeMcpProviderNames: activeMcpProviderNamesForResume,
logContext: checkpointLogContext,
});
};
Expand Down Expand Up @@ -1131,6 +1146,8 @@ export async function generateAssistantReply(
sliceId: currentSliceId,
allMessages: agent.state.messages,
loadedSkillNames: activeSkills.map((skill) => skill.name),
activeMcpProviderNames:
turnMcpToolManager?.getActiveProviders() ?? [],
logContext: checkpointLogContext,
});
}
Expand Down Expand Up @@ -1167,6 +1184,7 @@ export async function generateAssistantReply(
currentUsage: turnUsage,
messages: timeoutResumeMessages,
loadedSkillNames: loadedSkillNamesForResume,
activeMcpProviderNames: activeMcpProviderNamesForResume,
errorMessage: error instanceof Error ? error.message : String(error),
logContext: checkpointLogContext,
});
Expand Down Expand Up @@ -1204,6 +1222,7 @@ export async function generateAssistantReply(
currentUsage: turnUsage,
messages: timeoutResumeMessages,
loadedSkillNames: loadedSkillNamesForResume,
activeMcpProviderNames: activeMcpProviderNamesForResume,
errorMessage: error.message,
logContext: checkpointLogContext,
});
Expand Down
8 changes: 8 additions & 0 deletions packages/junior/src/chat/services/turn-checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export async function persistRunningCheckpoint(args: {
sliceId: number;
messages: PiMessage[];
loadedSkillNames: string[];
activeMcpProviderNames: string[];
logContext: CheckpointLogContext;
}): Promise<void> {
if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
Expand All @@ -132,6 +133,7 @@ export async function persistRunningCheckpoint(args: {
state: "running",
piMessages: args.messages,
loadedSkillNames: args.loadedSkillNames,
activeMcpProviderNames: args.activeMcpProviderNames,
});
} catch (checkpointError) {
logCheckpointError(
Expand All @@ -155,6 +157,7 @@ export async function persistCompletedCheckpoint(args: {
sliceId: number;
allMessages: PiMessage[];
loadedSkillNames: string[];
activeMcpProviderNames: string[];
logContext: CheckpointLogContext;
}): Promise<void> {
try {
Expand All @@ -177,6 +180,7 @@ export async function persistCompletedCheckpoint(args: {
state: "completed",
piMessages: args.allMessages,
loadedSkillNames: args.loadedSkillNames,
activeMcpProviderNames: args.activeMcpProviderNames,
});
} catch (checkpointError) {
logCheckpointError(
Expand All @@ -203,6 +207,7 @@ export async function persistAuthPauseCheckpoint(args: {
currentUsage?: AgentTurnUsage;
messages: PiMessage[];
loadedSkillNames: string[];
activeMcpProviderNames: string[];
errorMessage: string;
logContext: CheckpointLogContext;
}): Promise<AgentTurnSessionCheckpoint | undefined> {
Expand Down Expand Up @@ -232,6 +237,7 @@ export async function persistAuthPauseCheckpoint(args: {
state: "awaiting_resume",
piMessages,
loadedSkillNames: args.loadedSkillNames,
activeMcpProviderNames: args.activeMcpProviderNames,
resumeReason: "auth",
resumedFromSliceId: args.currentSliceId,
errorMessage: args.errorMessage,
Expand Down Expand Up @@ -263,6 +269,7 @@ export async function persistTimeoutCheckpoint(args: {
currentUsage?: AgentTurnUsage;
messages: PiMessage[];
loadedSkillNames: string[];
activeMcpProviderNames: string[];
errorMessage: string;
logContext: CheckpointLogContext;
}): Promise<AgentTurnSessionCheckpoint | undefined> {
Expand Down Expand Up @@ -293,6 +300,7 @@ export async function persistTimeoutCheckpoint(args: {
state: "awaiting_resume",
piMessages,
loadedSkillNames: args.loadedSkillNames,
activeMcpProviderNames: args.activeMcpProviderNames,
resumeReason: "timeout",
resumedFromSliceId: args.currentSliceId,
errorMessage: args.errorMessage,
Expand Down
Loading
Loading