Building MCP servers (the build-nanohub-mcp skill)
This section reproduces the build-nanohub-mcp skill that ships in this
repository. It walks through turning a simulation, solver, or analysis library
into a secure, bounded nanoHUB MCP tool — designing explicit tools, hardening
inputs, testing offline and in CI, packaging as a headless nanoHUB tool,
publishing through HubZero/com_mcp, connecting real MCP clients, and
diagnosing gateway, OAuth, session, CORS, Tasks, Resources, and MCP Apps
failures.
:::{admonition} Get the skill :class: tip
The skill is a directory of Markdown, scripts, and a worked example, not a single file. To use it with Claude Code or Claude:
Browse on GitHub: build-nanohub-mcp/
Download the whole repo (contains the skill) as a zip: nanohub-mcp main.zip — after unzipping, the skill is the
build-nanohub-mcp/folder.Or clone it:
git clone https://github.com/denphi/nanohub-mcp.git # skill lives at nanohub-mcp/build-nanohub-mcp/
Install it as a Claude skill by copying the build-nanohub-mcp/ directory into
your project’s .claude/skills/ (or your personal skills directory).
:::
Turn a simulation, solver, or analysis library into a nanoHUB tool that an MCP
host can drive conversationally. Produce one server with small, explicit tools,
offline scientific tests, a headless middleware/invoke, CI, and live gateway
verification.
Target architecture
MCP host (Claude, ChatGPT, Inspector, hub chat)
│ OAuth bearer token; the hub provides discovery and routing
▼
https://nanohub.org/api/mcp/{tool}/mcp
▼
com_mcp → nanoHUB session → middleware/invoke → start_mcp
▼
bin/{tool}.py → server = MCPServer(...)
Start from the safe scaffold:
python scripts/new_tool.py mysolver
Use examples/diffusion1d/ as the complete worked example. Replace its numerical kernel while retaining its schemas, run-handle confinement, workload checks, create→run→get decomposition, and tests.
1. Establish the security and state boundaries
Read references/security.md before exposing any
function. Treat every tool argument and every file read by a tool as untrusted.
Use opaque run handles rooted beneath one application-owned directory; never
accept a caller-selected filesystem path. Bound derived work, not just each
parameter. Invoke solvers with argument arrays, shell=False, and a timeout.
Never return credentials or present external text as model instructions.
Read references/state-model.md before caching.
Keep globals limited to immutable configuration and synchronized caches. Store
run state behind handles; key optional conversation state by ctx.session_id
with expiry, but never use an MCP session ID as authorization.
2. Design explicit tools
Split simulation workflows into:
create_<case>_sim(params) → {run_handle, inputs, estimate}: validate scientific inputs and derived cost, then write inputs beneath the run root.run_simulation(run_handle): make only this slow step an async tool.get_*readers: return bounded, plot-ready arrays, units, warnings, and pagination/decimation controls.delete_run(run_handle): resolve the handle through the same confinement helper and mark the tool destructive.
Declare input_schema and output_schema on every tool. Put types, units,
descriptions, defaults, enums, and numeric bounds in the input schema; repeat
critical validation inside the handler for direct calls and defense in depth.
Keep zero-argument defaults physically meaningful.
Use references/server-guide.md for tool contracts and references/tasks.md for async execution.
3. Test and validate locally
Install the same dependencies used by the published environment, then run:
python3 -m pytest tests/ -q
python scripts/validate_server.py bin/yourtool.py
start_mcp --app bin/yourtool.py --port 8000
Exercise initialize, the initialized notification, tools/list, a complete
create→run→get workflow, invalid schemas, traversal handles, workload limits,
and cleanup. Require at least one passing test and make skipped offline tests a
CI failure. The scaffold includes .github/workflows/ci.yml and copies the
validator into the generated repository.
4. Package and publish
Use this repository layout:
yourtool/
├── bin/yourtool.py
├── middleware/invoke
├── scripts/validate_server.py
├── tests/
├── .github/workflows/ci.yml
├── doc/description.html
└── src/ data/ examples/
Follow references/project-layout.md for the
environment-pinned invoke command and nanoHUB workflow. Drive the tool through
Uploaded → Installed → Approved → Published, then ask the hub team to register
it with com_mcp. Terminate stale sessions before validating a new revision.
5. Connect real clients
Do not stop at curl. Follow references/connecting-clients.md for Claude Code, Claude, ChatGPT, MCP Inspector, hub chat, and token-authenticated scripts. Check the dated host-capability notes before claiming support for Resources, Prompts, Tasks, Apps, or elicitation.
Run the non-mutating live checks first:
HUB=https://nanohub.org TOOL=yourtool scripts/smoke_live.sh
Add TOKEN=... to test an authenticated MCP session and resource reads. Set
TASK_TOOL=run_simulation plus TASK_ARGS_JSON='{"run_handle":"..."}' to
exercise Tasks. Set RUN_DCR=1 only when intentionally creating a throwaway
OAuth client; remove that client afterwards.
Use references/troubleshooting.md to map common symptoms to the failing layer. Use references/verification.md for the manual equivalents.
6. Add optional protocol features
Use references/mcp-apps.md for an interactive
ui://application after plain tools work.Use references/elicitation.md for capability- gated forms, confirmations, and URL flows.
Use references/quota-and-etiquette.md before shipping to constrain context, API calls, disk, and sessions.
Use references/versioning.md before changing a published schema, tool name, framework version, or hub revision.
Bundled resources
Resource |
Use |
|---|---|
|
Scaffold a secure server, tests, invoke file, validator, docs, and CI |
|
Validate contracts and flag common security hazards before deployment |
|
Verify discovery, CORS, OAuth metadata, sessions, resources, and optional Tasks/DCR |
Apply input, path, process, result, secret, and resource controls |
|
Separate global, run, session, and authenticated-user state |
|
Design tools, schemas, annotations, resources, prompts, and context use |
|
Choose sync/async behavior and test Tasks |
|
Package, invoke, install, and publish the nanoHUB tool |
|
Configure real MCP hosts and compare capabilities |
|
Diagnose symptom → cause → fix |
|
Run local and deployed checks manually |
|
Add an MCP App |
|
Ask for user input through the MCP session |
|
Control context, calls, storage, and session costs |
|
Understand hub-provided OAuth and DCR |
|
Diagnose gateway, CORS, SSE, and header forwarding |
|
Evolve protocol, framework, tools, and hub revisions safely |
Reference material
The steps above link into these reference pages, included here in full:
- Security and input hardening
- State model
- Writing the server: tools, schemas, resources, prompts
- Long-running work: async tools and the MCP Tasks extension
- Project layout, the invoke file, and how your server actually runs
- Connecting real MCP clients
- Troubleshooting playbook
- Live verification (do this before trusting any bug report — including your own)
- MCP Apps (interactive ui:// resources)
- Elicitation: asking the user for input mid-tool
- Quota and etiquette: tokens, API calls, sessions
- OAuth + Dynamic Client Registration on nanoHUB
- The com_mcp gateway: PHP realities, CORS, SSE, sessions
- Versioning: protocol, server, tools, and hub revisions