feat(mcp): add HTTP streaming transport and cancellation support#17161
feat(mcp): add HTTP streaming transport and cancellation support#17161fdncred merged 21 commits intonushell:mainfrom
Conversation
|
We would very much appreciate this for our developer environments that are accessed via SSH. Are there any blockers for this feature? |
I'm really busy but it is pretty straight forward if you want to finish it up related I will likely work on this soon |
Add configurable transport for MCP server with two options: - stdio (default): Standard I/O transport for local usage - http: Streamable HTTP with SSE for remote access New CLI flags: - `--mcp-transport <stdio|http>`: Select transport mode - `--mcp-port <port>`: Port for HTTP transport (default: 8080) Example usage: nu --mcp # stdio (default) nu --mcp --mcp-transport http # HTTP on port 8080 nu --mcp --mcp-transport http --mcp-port 3000 This enables remote MCP sessions over HTTP, which is a step toward supporting AI coding assistants working over network connections. Closes nushell#17160
c773c82 to
ffa5ea5
Compare
|
This works! ❯ cargo install --git https://github.com/andrewgazelka/nushell --branch feat/mcp-http-transport --features mcp --root . nu
Updating git repository `https://github.com/andrewgazelka/nushell`
Installing nu v0.110.1 (https://github.com/andrewgazelka/nushell?branch=feat%2Fmcp-http-transport#ad4d95ec)
...
Installed package `nu v0.110.1 (https://github.com/andrewgazelka/nushell?branch=feat%2Fmcp-http-transport#ad4d95ec)` (executable `nu.exe`)
warning: be sure to add `C:\Users\ofek\Desktop\bin` to your PATH to be able to run the installed binaries
❯ let id = (job spawn { ^.\bin\nu --mcp --mcp-transport http }); job spawn { sleep 5sec; job kill $id } | ignore
❯ mcp-inspector --cli --transport http http://localhost:8080/mcp/ --method tools/list
{
"tools": [
{
"name": "command_help",
"description": "Get help for a specific Nushell command. This will only work on commands that are native to nushell. To find out if a command is native to nushell you can use the find_command tool.",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"description": "The name of the command",
"type": "string"
}
},
"required": [
"name"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CommandNameRequest"
}
},
{
"name": "list_commands",
"description": "List available Nushell native commands.\nBy default all available commands will be returned. To find a specific command by searching command names, descriptions and search terms, use the find parameter.",
"inputSchema": {
"type": "object",
"properties": {
"find": {
"description": "string to find in command names, descriptions, and search term",
"type": "string",
"nullable": true
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "ListCommandsRequest"
}
},
{
"name": "evaluate",
"description": "Execute a command in the nushell.\nThis will return the output and error concatenated into a single string, as\nyou would see from running on the command line. There will also be an indication\nof if the command succeeded or failed.\n\nAvoid commands that produce a large amount of output, and consider piping those outputs to files.\nIf you need to run a long lived command, background it - e.g. `job spawn { uvicorn main:app }` so that\nthis tool does not run indefinitely.\n\nCommand Equivalents to Bash:\n\n| Bash Command | Nushell Command | Description |\n|--------------|-----------------|-------------|\n| `mkdir -p <path>` | `mkdir <path>` | Creates the given path, creating parents as necessary |\n| `> <path>` | `o> <path>` | Save command output to a file |\n| `>> <path>` | `o>> <path>` | Append command output to a file |\n| `> /dev/null` | `ignore` | Discard command output |\n| `> /dev/null 2>&1` | `o+e>\\| ignore` | Discard command output, including stderr |\n| `command 2>&1` | `command o+e>\\| ...` | Redirect stderr to stdout (use `o+e>` or `out+err>`) |\n| `cmd1 \\| tee log.txt \\| cmd2` | `cmd1 \\| tee { save log.txt } \\| cmd2` | Tee command output to a log file |\n| `command \\| head -5` | `command \\| first 5` | Limit the output to the first 5 rows of an internal command (see also last and skip) |\n| `cat <path>` | `open --raw <path>` | Display the contents of the given file |\n| `cat <(<command1>) <(<command2>)` | `[(command1), (command2)] \\| str join` | Concatenate the outputs of command1 and command2 |\n| `cat <path> <(<command>)` | `[(open --raw <path>), (command)] \\| str join` | Concatenate the contents of the given file and output of command |\n| `for f in *.md; do echo $f; done` | `ls *.md \\| each { $in.name }` | Iterate over a list and return results |\n| `for i in $(seq 1 10); do echo $i; done` | `for i in 1..10 { print $i }` | Iterate over a list and run a command on results |\n| `cp <source> <dest>` | `cp <source> <dest>` | Copy file to new location |\n| `rm -rf <path>` | `rm -r <path>` | Recursively removes the given path |\n| `date -d <date>` | `\"<date>\" \\| into datetime -f <format>` | Parse a date (format documentation) |\n| `sed` | `str replace` | Find and replace a pattern in a string |\n| `grep <pattern>` | `where $it =~ <substring>` or `find <substring>` | Filter strings that contain the substring |\n| `command1 && command2` | `command1; command2` | Run a command, and if it's successful run a second |\n| `stat $(which git)` | `stat ...(which git).path` | Use command output as argument for other command |\n| `echo /tmp/$RANDOM` | `$\"/tmp/(random int)\"` | Use command output in a string |\n| `cargo b --jobs=$(nproc)` | `cargo b $\"--jobs=(sys cpu \\| length)\"` | Use command output in an option |\n| `echo $PATH` | `$env.PATH (Non-Windows) or $env.Path (Windows)` | See the current path |\n| `echo $?` | `$env.LAST_EXIT_CODE` | See the exit status of the last executed command |\n| `export` | `$env` | List the current environment variables |\n| `FOO=BAR ./bin` | `FOO=BAR ./bin` | Update environment for a command |\n| `echo $FOO` | `$env.FOO` | Use environment variables |\n| `echo ${FOO:-fallback}` | `$env.FOO? \\| default \"ABC\"` | Use a fallback in place of an unset variable |\n| `type FOO` | `which FOO` | Display information about a command (builtin, alias, or executable) |\n| `\\` | `( <command> )` | A command can span multiple lines when wrapped with ( and ) |\n\nIf the polars commands are available, prefer it for working with parquet, jsonl, ndjson, csv files, and avro files.\nIt is much more efficient than the other Nushell commands or other non-nushell commands.\nIt exposes much of the functionality of the polars dataframe library. Start the pipeline with `plugin use polars`\n\nAn example of converting a nushell table output to a polars dataframe:\n```nu\nps | polars into-df | polars collect\n```\n\nAn example of converting a polars dataframe back to a nushell table in order to run other nushell commands:\n```nu\npolars open file.parquet | polars into-nu\n```\n\nAn example of opening a parquet file, selecting columns, and saving to a new parquet file:\n```nu\npolars open file.parquet | polars select name status | polars save file2\n```\n\n**Important**: The `glob` command should be used exclusively when you need to locate a file or a code reference,\nother solutions may produce too large output because of hidden files! For example *do not* use `find` or `ls -r`.\nUse command_help tool to learn more about the `glob` command.\n\n**Important**: Variables and environment changes persist across tool calls (REPL-style).\nYou can set a variable in one call and access it in subsequent calls:\n- `let x = 42` in one call, then `$x` in the next call returns 42\n- `$env.MY_VAR = \"hello\"` persists for later calls\n\nHowever, external processes run in their own environment. Use absolute paths when possible.\n",
"inputSchema": {
"type": "object",
"properties": {
"input": {
"description": "The Nushell source code to evaluate",
"type": "string"
}
},
"required": [
"input"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NuSourceRequest"
}
}
]
} |
|
It seems to me that most people will use the HTTP transport in which case the UX of requiring 2 |
I do not think this is the case. for local development stdio will be the main use case as they do not have to run the server in the background :/ there are a lot of other stdio issues though. if there is a good way to not have to have a server terminal window in the background I am all ears |
Configure explicit session management to prevent memory growth from abandoned sessions. Sessions now automatically clean up after 30 minutes of inactivity. - Add SESSION_KEEP_ALIVE (30 min) for idle session cleanup - Explicitly configure LocalSessionManager and StreamableHttpServerConfig - Add tokio-util dependency for CancellationToken
c0692c9 to
d122cbd
Compare
External commands (e.g., `rg`, `grep`, `ls`) were outputting to the server's stdout instead of being captured and returned via MCP. This happened because `Stack::new()` defaults stdout to `OutDest::Inherit`, which sends external command output directly to the process's terminal. The output appeared in server debug logs but was never forwarded to clients. Fix: Call `.collect_value()` on the stack to set `pipe_stdout` to `OutDest::Value`, which captures external command output into the pipeline for proper MCP response handling.
Document how to strip ANSI codes using `ansi strip` and clarify that nushell doesn't support `\uXXXX` unicode escape syntax. Also mention the `char` command for producing special characters.
The CancellationToken was created but never cancelled on shutdown. When Ctrl-C was pressed, axum stopped accepting new connections but existing SSE streams and session handlers kept running indefinitely. Fix: Cancel the token in the graceful shutdown handler so all active sessions and streams are properly terminated.
|
My only real concern here is with adding a new dependency on axum. Is there any way to avoid that? |
Add `Stack::capture_all()` method that sets both `pipe_stdout` and `pipe_stderr` to `OutDest::Value`, capturing all output instead of letting it inherit to the process's terminal. This fixes commands like `cargo clippy`, `cargo build`, `rustc` etc. that output to stderr - their output is now returned via MCP instead of going to the server's terminal.
@fdncred I think streamable http using the MCP official rust client requires axum; let me double check if there is an alternative. Is the concern mostly bundle size or? axum is pretty standard for web servers these days and maintained by tokio team. |
looks like we can use hyper! let me use that |
Replace direct axum dependency with hyper-util, using TowerToHyperService to wrap the StreamableHttpService. This addresses the concern about adding axum as a dependency. The StreamableHttpService is a tower service that works with any HTTP server. We now use hyper directly via hyper-util instead of axum's convenience wrappers. Note: axum is still pulled in transitively by rmcp's transport-streamable-http-server feature, but we no longer depend on it directly.
Update to latest rmcp version.
b2982d4 to
87e0b0c
Compare
Parse and compile errors are user input errors, not server internal errors. Use JSON-RPC error code -32602 (Invalid params) instead of -32603 (Internal error) for these cases. This provides clearer semantics: internal_error is reserved for unexpected server-side failures, while invalid_params indicates the user's input was malformed.
87e0b0c to
e1ab4f6
Compare
…rmat Replace human-readable display error format with machine-readable NUON for MCP error responses. The new format extracts structured data from miette Diagnostic errors including: - Error code (e.g., "nu::parser::parse_mismatch") - Error message - Severity level (error/warning/advice) - Help/hint text - Documentation URL (if available) - Labels with line/column numbers and source context This makes errors much more useful for LLMs which can parse the structured data directly instead of extracting from formatted text.
Add `no_stdin` flag to EngineState that tells external commands to use Stdio::null() instead of Stdio::inherit() for stdin. This prevents commands like psql, sudo, or cat from hanging indefinitely when they prompt for passwords or other input in non-interactive contexts. MCP servers now set this flag on initialization since they run as servers without access to interactive stdin.
SSH bypasses stdin and opens /dev/tty directly for password prompts. Set SSH_ASKPASS="" and SSH_ASKPASS_REQUIRE=never to make SSH fail fast with a clear error instead of hanging or showing GUI prompts.
Detach from controlling terminal on Unix systems to prevent child processes from prompting for input. Programs like ssh, sudo, and psql bypass stdin and open /dev/tty directly for password prompts. By calling setsid(), we create a new session without a controlling terminal, making /dev/tty unavailable and causing these programs to fail fast with clear errors instead of hanging. This replaces the SSH_ASKPASS env var workaround with a universal fix. See: https://man7.org/linux/man-pages/man2/setsid.2.html
|
Heads up that the most recent change produces a much larger binary than the axum approach from 2 days ago. |
Fixes integer overflow vulnerability in BytesMut::reserve.
this is expected as axum is still included as a transitive dep (I have an open PR to fix this) |
SourceSpan implements Copy, so dereferencing is preferred over cloning.
The concern is about adding another dependency in general and how much size it adds to the binary. |
I was able to properly remove it btw |
Implement proper cancellation handling for MCP tool calls: - Fork state before evaluation using Arc-based copy-on-write - Run eval in spawn_blocking with its own Signals instance - Monitor CancellationToken and trigger interrupt on cancel - On success: commit forked state back to main state - On cancel: discard forked state, original unchanged This prevents long-running commands (e.g., `sleep 100000sec`) from blocking the MCP server indefinitely. When a client times out, the server can now interrupt and discard the operation cleanly.
Replace the generic no_stdin flag with is_mcp, following the same pattern as is_lsp. This makes the flag specific to MCP context and exposes it as $nu.is-mcp for runtime checks. In the future, a more general option like no_stdin could be added after more testing, but for now keeping it MCP-specific is safer.
|
I'd guess the CI error is because you added is_mcp, probably just need to increment the count. |
Add is-mcp to the expected completions list and update the count to 21 to match the new field added in the eval_const.rs
|
Let's move forward with this. Thanks! |
|
Thanks a lot! @fdncred I am curious what you think of my comment here. As a user of the remote MCP functionality, requiring both I would suggest that we either go with the |
This PR was already getting long in the tooth so I wanted to get it to mvp and run with it. For what it's worth, let me know if I'm wrong, but it seems to me that having the parameters the way they are now isn't that big of a deal since you configure whatever you want to use the mcp once. I wouldn't think it would be typed dozens of times in the repl. Having said that, I wouldn't be opposed to |
|
I'd second a follow-up to this where we compress the redundant args. |
I do not have a preference on any of this i will not be offended if someone makes a change based on what is thought to be best |
|
if someone is passionate about this parameter change, please step forward with a pr. thanks for the conversation and continuing to make nushell better. |
Summary
Add HTTP streaming transport for MCP server with cancellation support and multiple improvements for production use.
Features
--mcp-transport http)--mcp-portto set custom port (default: 8080)Cancellation Architecture
When a client times out or cancels a request:
This prevents commands like
sleep 100000secfrom blocking the MCP server indefinitely.Fixes
setsid()to prevent ssh/sudo/psql from hanging on password prompts (man page)no_stdinflag so commands don't inherit stdininvalid_paramserror codeImprovements
Usage
Motivation
This is the first step toward #17160 — enabling remote AI coding sessions.
With HTTP transport, you can SSH to a remote machine, start
nu --mcp --mcp-transport http, and connect an AI coding assistant to that endpoint over the network.Test plan
nu --mcpworks as before (stdio)nu --mcp --mcp-transport httpstarts HTTP servercat,psql,ssh)🤖 Generated with Claude Code