Problem
jcode's MCP client already knows that servers advertise prompts. crates/jcode-base/src/mcp/protocol.rs:83 deserializes it:
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ServerCapabilities {
#[serde(default)]
pub tools: Option<ToolsCapability>,
#[serde(default)]
pub resources: Option<ResourcesCapability>,
#[serde(default)]
pub prompts: Option<PromptsCapability>,
}
and McpClient::initialize stores the whole struct on the shared handle at client.rs:253. But no code anywhere in the tree ever sends prompts/list or prompts/get. The only JSON-RPC methods the client issues are initialize, notifications/initialized, tools/list, and tools/call. The prompts capability is parsed and then discarded.
Prompts are the half of MCP that is meant for the human, not the model. A server ships a named, parameterized, server-maintained workflow, and the client surfaces it where the user types. That is why every comparable harness exposes them as slash commands:
- Claude Code registers them as
/mcp__<server>__<prompt>, for example /mcp__github__create-issue.
- opencode registers them as
<server>:<prompt> slash commands.
- VS Code Copilot exposes them in its prompt picker.
- Codex does not have them yet, and the request is its issue #8342 with 23 reactions.
For jcode specifically the gap is larger than for a greenfield client, because jcode imports MCP servers from other harnesses on first run. McpConfig::import_from_external reads ~/.claude.json, ~/.claude/mcp.json, and ~/.codex/config.toml. A user arriving from Claude Code brings servers whose prompts they were already invoking by name, and in jcode those prompts become invisible with no message explaining why.
The shape jcode needs is already in the codebase twice over. crates/jcode-base/src/mcp/tool.rs namespaces MCP tools as mcp__{server}__{tool}, which is exactly Claude Code's prompt naming. And crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs:345 already appends dynamic slash commands to the static table, for skills:
fn push_skill_commands(
commands: &mut Vec<(String, &'static str)>,
seen: &mut std::collections::HashSet<String>,
skills: &crate::skill::SkillRegistry,
) {
for skill in skills.list() {
let command = format!("/{}", skill.name);
if seen.insert(command.clone()) {
commands.push((command, "Activate skill"));
}
}
}
MCP prompts are the same pattern against a different registry.
This feature was proposed from code analysis without any hands-on usage.
Proposed approach
crates/jcode-base/src/mcp/protocol.rs (implementation)
Add the prompt types next to the existing McpToolDef / ToolsListResult / ToolCallParams block, in the same style:
/// MCP prompt definition from server (`prompts/list`)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpPromptDef {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub arguments: Vec<McpPromptArgument>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpPromptArgument {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub required: bool,
}
/// prompts/list result
#[derive(Debug, Clone, Deserialize)]
pub struct PromptsListResult {
pub prompts: Vec<McpPromptDef>,
}
/// prompts/get params
#[derive(Debug, Clone, Serialize)]
pub struct PromptGetParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
}
/// prompts/get result
#[derive(Debug, Clone, Deserialize)]
pub struct PromptGetResult {
#[serde(default)]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PromptMessage {
pub role: String,
pub content: ContentBlock,
}
ContentBlock already exists in this file and already covers text, image, and resource, so prompt message content reuses it directly.
Also declare the prompts capability on the client side. ClientCapabilities is currently an empty struct at line 57; leave it empty for this change, since the client is a consumer and does not need to advertise anything to receive prompts.
crates/jcode-base/src/mcp/client.rs (implementation)
McpHandle already holds capabilities: Arc<RwLock<ServerCapabilities>> and tools: Arc<RwLock<Vec<McpToolDef>>>. Add a sibling prompts: Arc<RwLock<Vec<McpPromptDef>>> initialized at the handle construction site (line 246) and three methods mirroring the existing tool methods:
pub fn supports_prompts(&self) -> bool // reads capabilities.prompts.is_some()
pub fn prompts(&self) -> Vec<McpPromptDef> // mirrors tools()
pub async fn refresh_prompts(&self) -> Result<()> // mirrors refresh_tools(), sends "prompts/list"
pub async fn get_prompt(&self, name: &str, arguments: Option<Value>) -> Result<PromptGetResult>
In McpClient::connect_in_dir, after the existing refresh_tools() call at line 266, add:
if client.handle.supports_prompts()
&& let Err(e) = client.handle.refresh_prompts().await
{
crate::logging::warn(&format!(
"MCP server '{}' advertised prompts but prompts/list failed: {}",
name, e
));
}
Not ?. A prompts failure must not fail the connection, and the swallowed-error ratchet requires the error to be reported rather than dropped.
crates/jcode-base/src/mcp/manager.rs (implementation)
Mirror the existing all_tools() / call_tool() pair with all_prompts() -> Vec<(String, McpPromptDef)> and get_prompt(server, name, arguments), resolving across both pool_handles and owned_clients exactly as the tool path does.
crates/jcode-base/src/mcp/mod.rs (wiring)
pub use protocol::*; already re-exports the whole protocol module, so the new types are exported with no edit. Add the prompt registry helper to the existing pub use tool::{...} line only if a helper is introduced there.
crates/jcode-tui/src/tui/app.rs (wiring)
Add mcp_prompt_names: Vec<(String, String)> next to the existing mcp_server_names: Vec<(String, usize)> field (line 1154 region), holding (server, prompt) pairs. Initialize it in both constructors in crates/jcode-tui/src/tui/app/tui_lifecycle.rs (lines 525 and 964, where remote_mcp_servers: Vec::new() is already initialized).
crates/jcode-tui/src/tui/app/tui_lifecycle_runtime.rs (wiring)
init_mcp already populates self.mcp_server_names from manager.connected_servers() and manager.all_tools() (lines 252-264). Populate self.mcp_prompt_names from manager.all_prompts() in the same block, then call self.invalidate_command_candidates_cache() so the freshly connected servers' prompts appear in the palette without waiting for another invalidation.
crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs (wiring)
In command_candidates() (line 340), alongside push_skill_commands, add:
fn push_mcp_prompt_commands(
commands: &mut Vec<(String, &'static str)>,
seen: &mut std::collections::HashSet<String>,
prompts: &[(String, String)],
) {
for (server, prompt) in prompts {
let command = format!("/mcp__{}__{}", server, prompt);
if seen.insert(command.clone()) {
commands.push((command, "Run MCP prompt"));
}
}
}
The help string must be &'static str to match the existing tuple type, which is why it is a fixed label rather than the server-supplied description. The existing CommandCandidatesCache and invalidate_command_candidates_cache() already handle memoization.
crates/jcode-tui/src/tui/app/input.rs (wiring, dispatch)
Insert a branch before the skill resolution block at line 3514 (let initial_snapshot = self.current_skills_snapshot();), so an /mcp__... command never falls through to "Unknown skill". Parse /mcp__<server>__<prompt> with optional trailing text, call manager.get_prompt, concatenate the PromptMessage text blocks, and submit the result as the turn input.
Failure paths, each a DisplayMessage::error rather than a panic or a silent drop, matching how the unknown-skill path already reports:
- unknown server or prompt name
prompts/get returned an error
- the prompt declares more than one required argument, in which case list the argument names and stop
Remote sessions: is_remote clients do not hold a local mcp_manager. Follow the existing precedent at line 3495, where input-line ! shell commands report "only available in a local jcode TUI session", and report the same for MCP prompts in this change. Wiring prompts through the remote protocol is a follow-up.
crates/jcode-base/src/mcp/protocol_tests.rs (tests)
Already included via #[path = "protocol_tests.rs"] mod protocol_tests; at protocol.rs:555. Add:
prompts_list_result_deserializes_arguments including a prompt with no arguments key.
prompt_get_result_deserializes_text_and_resource_messages, exercising the reused ContentBlock variants.
prompt_get_params_omits_arguments_when_none, asserting the serialized JSON has no arguments key.
crates/jcode-tui/src/tui/app/tests/mcp_prompt_commands.rs (tests, new file)
Register with include!("tests/mcp_prompt_commands.rs"); in crates/jcode-tui/src/tui/app/tests.rs, matching the existing pattern used for issue_496_input_routing.rs and skill_invocation_multi_word.rs.
- With
mcp_prompt_names populated, /mcp__ produces the expected suggestion entries.
- An unknown
/mcp__server__prompt produces an error display message and does not produce "Unknown skill".
- A prompt command in a remote session reports the local-only message.
crates/jcode-base/src/mcp/manager.rs:623 already documents a stub-server harness that answers initialize, tools/list, and tools/call with canned JSON-RPC replies. Extend that stub with a prompts/list and prompts/get case rather than adding a new fixture.
Estimated scope: roughly 90 lines in protocol.rs, 70 in client.rs, 50 in manager.rs, 30 across the TUI state and lifecycle files, 70 in input.rs, and about 160 lines of tests.
Willingness to implement
I'm happy to open a PR implementing this if the direction works for you.
Problem
jcode's MCP client already knows that servers advertise prompts.
crates/jcode-base/src/mcp/protocol.rs:83deserializes it:and
McpClient::initializestores the whole struct on the shared handle atclient.rs:253. But no code anywhere in the tree ever sendsprompts/listorprompts/get. The only JSON-RPC methods the client issues areinitialize,notifications/initialized,tools/list, andtools/call. The prompts capability is parsed and then discarded.Prompts are the half of MCP that is meant for the human, not the model. A server ships a named, parameterized, server-maintained workflow, and the client surfaces it where the user types. That is why every comparable harness exposes them as slash commands:
/mcp__<server>__<prompt>, for example/mcp__github__create-issue.<server>:<prompt>slash commands.For jcode specifically the gap is larger than for a greenfield client, because jcode imports MCP servers from other harnesses on first run.
McpConfig::import_from_externalreads~/.claude.json,~/.claude/mcp.json, and~/.codex/config.toml. A user arriving from Claude Code brings servers whose prompts they were already invoking by name, and in jcode those prompts become invisible with no message explaining why.The shape jcode needs is already in the codebase twice over.
crates/jcode-base/src/mcp/tool.rsnamespaces MCP tools asmcp__{server}__{tool}, which is exactly Claude Code's prompt naming. Andcrates/jcode-tui/src/tui/app/state_ui_input_helpers.rs:345already appends dynamic slash commands to the static table, for skills:MCP prompts are the same pattern against a different registry.
Proposed approach
crates/jcode-base/src/mcp/protocol.rs(implementation)Add the prompt types next to the existing
McpToolDef/ToolsListResult/ToolCallParamsblock, in the same style:ContentBlockalready exists in this file and already coverstext,image, andresource, so prompt message content reuses it directly.Also declare the prompts capability on the client side.
ClientCapabilitiesis currently an empty struct at line 57; leave it empty for this change, since the client is a consumer and does not need to advertise anything to receive prompts.crates/jcode-base/src/mcp/client.rs(implementation)McpHandlealready holdscapabilities: Arc<RwLock<ServerCapabilities>>andtools: Arc<RwLock<Vec<McpToolDef>>>. Add a siblingprompts: Arc<RwLock<Vec<McpPromptDef>>>initialized at the handle construction site (line 246) and three methods mirroring the existing tool methods:In
McpClient::connect_in_dir, after the existingrefresh_tools()call at line 266, add:Not
?. A prompts failure must not fail the connection, and the swallowed-error ratchet requires the error to be reported rather than dropped.crates/jcode-base/src/mcp/manager.rs(implementation)Mirror the existing
all_tools()/call_tool()pair withall_prompts() -> Vec<(String, McpPromptDef)>andget_prompt(server, name, arguments), resolving across bothpool_handlesandowned_clientsexactly as the tool path does.crates/jcode-base/src/mcp/mod.rs(wiring)pub use protocol::*;already re-exports the whole protocol module, so the new types are exported with no edit. Add the prompt registry helper to the existingpub use tool::{...}line only if a helper is introduced there.crates/jcode-tui/src/tui/app.rs(wiring)Add
mcp_prompt_names: Vec<(String, String)>next to the existingmcp_server_names: Vec<(String, usize)>field (line 1154 region), holding(server, prompt)pairs. Initialize it in both constructors incrates/jcode-tui/src/tui/app/tui_lifecycle.rs(lines 525 and 964, whereremote_mcp_servers: Vec::new()is already initialized).crates/jcode-tui/src/tui/app/tui_lifecycle_runtime.rs(wiring)init_mcpalready populatesself.mcp_server_namesfrommanager.connected_servers()andmanager.all_tools()(lines 252-264). Populateself.mcp_prompt_namesfrommanager.all_prompts()in the same block, then callself.invalidate_command_candidates_cache()so the freshly connected servers' prompts appear in the palette without waiting for another invalidation.crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs(wiring)In
command_candidates()(line 340), alongsidepush_skill_commands, add:The help string must be
&'static strto match the existing tuple type, which is why it is a fixed label rather than the server-supplied description. The existingCommandCandidatesCacheandinvalidate_command_candidates_cache()already handle memoization.crates/jcode-tui/src/tui/app/input.rs(wiring, dispatch)Insert a branch before the skill resolution block at line 3514 (
let initial_snapshot = self.current_skills_snapshot();), so an/mcp__...command never falls through to "Unknown skill". Parse/mcp__<server>__<prompt>with optional trailing text, callmanager.get_prompt, concatenate thePromptMessagetext blocks, and submit the result as the turn input.Failure paths, each a
DisplayMessage::errorrather than a panic or a silent drop, matching how the unknown-skill path already reports:prompts/getreturned an errorRemote sessions:
is_remoteclients do not hold a localmcp_manager. Follow the existing precedent at line 3495, where input-line!shell commands report "only available in a local jcode TUI session", and report the same for MCP prompts in this change. Wiring prompts through the remote protocol is a follow-up.crates/jcode-base/src/mcp/protocol_tests.rs(tests)Already included via
#[path = "protocol_tests.rs"] mod protocol_tests;atprotocol.rs:555. Add:prompts_list_result_deserializes_argumentsincluding a prompt with noargumentskey.prompt_get_result_deserializes_text_and_resource_messages, exercising the reusedContentBlockvariants.prompt_get_params_omits_arguments_when_none, asserting the serialized JSON has noargumentskey.crates/jcode-tui/src/tui/app/tests/mcp_prompt_commands.rs(tests, new file)Register with
include!("tests/mcp_prompt_commands.rs");incrates/jcode-tui/src/tui/app/tests.rs, matching the existing pattern used forissue_496_input_routing.rsandskill_invocation_multi_word.rs.mcp_prompt_namespopulated,/mcp__produces the expected suggestion entries./mcp__server__promptproduces an error display message and does not produce "Unknown skill".crates/jcode-base/src/mcp/manager.rs:623already documents a stub-server harness that answersinitialize,tools/list, andtools/callwith canned JSON-RPC replies. Extend that stub with aprompts/listandprompts/getcase rather than adding a new fixture.Estimated scope: roughly 90 lines in
protocol.rs, 70 inclient.rs, 50 inmanager.rs, 30 across the TUI state and lifecycle files, 70 ininput.rs, and about 160 lines of tests.Willingness to implement
I'm happy to open a PR implementing this if the direction works for you.