Project layout, the invoke file, and how your server actually runs

Contents

  • Repository layout

  • middleware/invoke

  • start_mcp behavior

  • Publishing and revisions

Repository layout of a nanoHUB MCP tool

A nanoHUB MCP tool is a regular nanoHUB tool. Real examples: padremcp (minimal) and rappturemcp (large).

yourtool/
├── bin/
│   ├── yourtool.py          # THE server — defines module-level `server`
│   └── test_offline.py      # tests that need no solver/session (run anywhere)
├── middleware/
│   └── invoke               # how the hub launches the tool (see below)
├── scripts/
│   ├── validate_server.py   # copied by the scaffold
│   └── check_ci.py          # fail CI on skipped/empty suites
├── .github/workflows/ci.yml # offline tests + validator on push/PR
├── doc/
│   └── description.html     # the tool's page on nanohub.org
├── examples/                # example inputs / notebooks
├── data/                    # static data the tools read
├── src/                     # your scientific library code, or a Makefile
└── README.md

Rules of thumb:

  • Everything the server imports at runtime must be importable inside the tool session — either in bin/ next to the server file (start_mcp adds that directory to sys.path), installed in the conda env named by the invoke file, or an installed hub app.

  • Keep the server in ONE file if you can. Deployment, debugging, and the @tool/bin/... invoke reference all get simpler.

  • Offline tests belong in the repo and must not require the solver binary or a hub session (test deck generation, schemas, tool registration — see scripts/ in this skill).

  • Keep CI offline. Do not run smoke_live.sh automatically: DCR and deployed solver calls may mutate production state and require explicit credentials.

The middleware/invoke file, flag by flag

Minimal (padremcp):

#!/bin/sh
/usr/bin/invoke_app "$@" -t padremcp \
                         -C "start_mcp --app @tool/bin/padremcp.py" \
                         -u anaconda-6 \
                         -u padre-2.4E-r15 \
                         -r none \
                         -w headless

Flag

Meaning

-t yourtool

tool name — must match the published tool (and the gateway URL /api/mcp/{yourtool}/mcp)

-C "start_mcp --app @tool/bin/yourtool.py"

the command to run; @tool expands to the installed tool directory

-u <env>

environment module(s) to load — repeatable; load your conda distribution AND your solver (e.g. anaconda-6 + padre-2.4E-r15)

-r none

no Rappture wrapper

-w headless

no VNC desktop — an MCP server has no GUI; sessions are cheaper and start faster

-e NAME=VALUE

extra environment variables (e.g. PYTHONNOUSERSITE=1 to keep user site-packages from shadowing the env)

The env-pinning gotcha (will bite you): a bare start_mcp resolves against whatever the loaded anaconda base provides. If your dependencies live in a specific conda env, pin the absolute path:

-C "/apps/share64/debian7/anaconda/anaconda-6/envs/yourenv/bin/start_mcp --app @tool/bin/yourtool.py"

Symptom of getting this wrong: the server starts and lists tools fine, but calls that import your library fail with No module named '...' — often surfacing only inside resources/read.

What start_mcp does (so you can debug it)

start_mcp (entry point of the nanohub-mcp package, nanohubmcp/cli.py):

  1. Imports your --app file and takes its module-level server variable (must be an MCPServer instance — this name is the contract).

  2. On nanoHUB (detected via SESSION/SESSIONDIR env): reads the session resources file, computes the proxy path /weber/{session}/{cookie}/{port%1000}/, runs the MCP server on port 8001 and the wrwroxy reverse proxy on port 8000. The gateway reaches the server through that weber path — this is why resource URIs sometimes arrive with a proxied prefix (the framework strips it).

  3. Locally (no session env): serves directly on --port (default 8000).

  4. --python-env NAME re-executes under a named conda env’s python — an alternative to hardcoding the env path in the invoke line.

Local development loop:

start_mcp --app bin/yourtool.py --port 8000
curl -s localhost:8000 -X POST -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | python3 -m json.tool

This is byte-for-byte the server that runs on the hub; anything you can’t make work locally will not work deployed.

Publishing on the hub

  1. Create the tool at nanohub.org/tools/create; pick a short lowercase name (it becomes the URL and the -t flag).

  2. Commit the layout above to the tool repo; drive the tool status pipeline (Uploaded → Installed → Approved → Published). Headless tools skip the screenshot/GUI review steps’ complications.

  3. Test the installed revision in a workspace/session before publishing: invoke should leave a running server you can curl through the session.

  4. Ask the hub team to register the tool with com_mcp (the MCP gateway component). After that, https://{hub}/api/mcp/{yourtool}/mcp is live and gated by com_mcp’s allowed_groups configuration.

  5. Re-verify through the gateway with the smoke tests (verification.md / scripts/smoke_live.sh) — the session-side server working does not guarantee the gateway path does.

Updating: publish a new tool revision; the gateway picks a fresh session. Old sessions may keep running the previous revision — kill stale sessions when testing an update, or you will debug the wrong code.