diff --git a/README.md b/README.md index 68f0489..e159bdc 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,59 @@ # Jungle Grid MCP Server -Run Jungle Grid GPU workloads from MCP-aware AI hosts such as Claude Desktop, -Cursor, Windsurf, and MCP Inspector. +Jungle Grid MCP lets MCP-aware agents estimate, submit, monitor, cancel, and retrieve artifacts from Jungle Grid workloads. It supports local stdio clients and hosted Streamable HTTP deployments that forward tool calls to the Jungle Grid API. -The server runs locally over stdio and forwards tool calls to the Jungle Grid -REST API with your API key. +Use it for asynchronous AI workload execution, batch processing, training, fine-tuning, uploaded file or script backed jobs, lifecycle diagnostics, workload logs, and managed output artifacts. -## Requirements +## Installation -- Node.js 18 or newer -- A Jungle Grid API key -- Optional: `JUNGLE_GRID_API_URL` for a self-hosted orchestrator +Requirements: -For the full submit workflow, the API key needs `jobs:write`. That scope allows -estimate, submit, polling, cancellation, and logs for jobs owned by the key's -account. `list_jobs` still requires `jobs:read`. +- Node.js 18 or newer +- A Jungle Grid API key for local stdio, or an OAuth bearer token for hosted HTTP +- API scopes that match the tools you want to call -## Quick Start +Run the local stdio server with `npx`: ```sh -JUNGLE_GRID_API_KEY=jg_... npx -y @jungle-grid/mcp +JUNGLE_GRID_API_KEY=jg_placeholder npx -y @jungle-grid/mcp ``` -On Windows PowerShell: +Install globally if you prefer a stable executable: -```powershell -$env:JUNGLE_GRID_API_KEY = "jg_..." -npx -y @jungle-grid/mcp +```sh +npm install -g @jungle-grid/mcp +junglegrid-mcp ``` -The server uses stdio, so a successful manual launch appears to wait for MCP -messages. If `JUNGLE_GRID_API_KEY` is missing, it exits with a clear error. +## Configuration + +Local stdio uses environment variables: + +| Variable | Required | Purpose | +| --- | --- | --- | +| `JUNGLE_GRID_API_KEY` | Yes for local stdio | Bearer token forwarded to the Jungle Grid API. | +| `JUNGLEGRID_API_BASE` | No | API base URL. Defaults to `https://api.junglegrid.dev`. | +| `JUNGLE_GRID_API_URL` | No | Legacy API base URL alias, also accepted. | -## Claude Desktop +Hosted HTTP gateway deployments also support: -Add this to `claude_desktop_config.json`, then fully restart Claude Desktop. +| Variable | Required | Purpose | +| --- | --- | --- | +| `MCP_TRANSPORT=http` | No | Starts Streamable HTTP instead of stdio. | +| `PORT` | No | HTTP port. Defaults to `3000`. | +| `JUNGLEGRID_INTERNAL_SERVICE_TOKEN` | No | Service token used for OAuth introspection or fallback API calls. | +| `OAUTH_ISSUER` | No | OAuth issuer. Defaults to `https://api.junglegrid.dev`. | +| `MCP_RESOURCE` | No | Protected resource URL. Defaults to `https://mcp.junglegrid.dev`. | +| `MCP_RESOURCE_METADATA_URL` | No | OAuth protected-resource metadata URL. | +| `OPENAI_APPS_CHALLENGE_TOKEN` | No | Enables `/.well-known/openai-apps-challenge` when configured. | + +Never commit API keys, OAuth tokens, signed upload URLs, signed artifact URLs, or callback secrets. + +## Connection Modes + +### Local stdio + +Local clients launch the package and communicate over stdio. ```json { @@ -43,29 +62,32 @@ Add this to `claude_desktop_config.json`, then fully restart Claude Desktop. "command": "npx", "args": ["-y", "@jungle-grid/mcp"], "env": { - "JUNGLE_GRID_API_KEY": "jg_..." + "JUNGLE_GRID_API_KEY": "jg_placeholder" } } } } ``` -Windows config path: +### Claude Desktop + +Add the same `mcpServers` block to `claude_desktop_config.json`, then fully quit and reopen Claude Desktop. + +macOS: ```text -%APPDATA%\Claude\claude_desktop_config.json +~/Library/Application Support/Claude/claude_desktop_config.json ``` -macOS config path: +Windows: ```text -~/Library/Application Support/Claude/claude_desktop_config.json +%APPDATA%\Claude\claude_desktop_config.json ``` -## Cursor or Project MCP Config +### Cursor -For a checked-in project config, avoid committing secrets. Put the API key in -the environment used to launch Cursor and keep the config secret-free. +For project config, avoid checked-in secrets. Put the key in the environment used to launch Cursor: ```json { @@ -78,7 +100,7 @@ the environment used to launch Cursor and keep the config secret-free. } ``` -For a local, uncommitted config, you can include the key directly: +For a local uncommitted Cursor config: ```json { @@ -87,73 +109,287 @@ For a local, uncommitted config, you can include the key directly: "command": "npx", "args": ["-y", "@jungle-grid/mcp"], "env": { - "JUNGLE_GRID_API_KEY": "jg_..." + "JUNGLE_GRID_API_KEY": "jg_placeholder", + "JUNGLEGRID_API_BASE": "https://api.junglegrid.dev" } } } } ``` -## Self-Hosted Orchestrator +### Hosted HTTP -`JUNGLE_GRID_API_URL` defaults to -`https://api.junglegrid.dev`. Override it when your host should -call a different orchestrator. +The HTTP server exposes: + +- `GET /healthz` +- `GET /.well-known/oauth-protected-resource` +- `POST /mcp` + +Start it locally: + +```sh +MCP_TRANSPORT=http PORT=3000 JUNGLEGRID_INTERNAL_SERVICE_TOKEN=service_token_placeholder npm start +``` + +Hosted MCP clients must send `Authorization: Bearer ` to `POST /mcp`. The server introspects tokens at `/oauth/introspect` on the configured API base and requires tool-specific scopes. + +## Minimal Working Example + +Ask your MCP client to call the tools in this order: ```json { - "mcpServers": { - "junglegrid": { - "command": "npx", - "args": ["-y", "@jungle-grid/mcp"], - "env": { - "JUNGLE_GRID_API_KEY": "jg_...", - "JUNGLE_GRID_API_URL": "https://your-orchestrator.example.com" - } - } + "tool": "estimate_job", + "arguments": { + "workload_type": "batch", + "image": "python:3.11-slim", + "command": ["python", "-c", "print('hello from Jungle Grid')"], + "routing_mode": "balanced" } } ``` -## Tools +If the estimate is acceptable, submit the job: + +```json +{ + "tool": "submit_job", + "arguments": { + "name": "mcp-hello", + "workload_type": "batch", + "image": "python:3.11-slim", + "command": ["python", "-c", "print('hello from Jungle Grid')"], + "expected_artifacts": ["/workspace/artifacts/output.txt"] + } +} +``` + +Use the returned `job_id` with `get_job`, `get_job_events`, `get_job_logs`, `list_artifacts`, and `get_artifact`. + +## MCP Tools + +The current tool registry exposes these exact tool names: -- `estimate_job`: estimate GPU tier, region, duration, and credit cost. -- `submit_job`: submit an asynchronous GPU workload with optional `environment` values. -- `upload_job_input`: create a signed upload slot for input files or scripts. -- `list_job_inputs`: list uploaded inputs and their mount paths. -- `get_job`: fetch current job status and details. -- `get_job_events`: fetch platform lifecycle events, including scheduling/startup events before workload logs exist. -- `list_jobs`: list recent jobs for the authenticated account. -- `cancel_job`: cancel a pending, queued, or running job. -- `get_job_logs`: fetch paginated logs with `cursor`, `limit`, and `tail`. -- `list_artifacts`: list managed artifacts uploaded for a job. -- `get_artifact`: create a signed download URL for one managed artifact. +| Tool | Purpose | Required parameters | Optional parameters | +| --- | --- | --- | --- | +| `estimate_job` | Estimate routing, capacity source, and expected cost without creating work. | `workload_type` | `model_size`, `image`, `command`, `args`, `routing_mode`, `template`, `notes` | +| `submit_job` | Submit a workload. This may start compute and incur usage charges. | `name`, `workload_type`, `image` | `command`, `args`, `env`, `input_files`, `script_files`, `script_file`, `expected_artifacts`, `routing_mode`, `template`, `metadata` | +| `upload_job_input` | Create a signed upload slot for an input file or script. | `filename` | `content_type`, `kind` | +| `list_job_inputs` | List uploaded inputs and scripts for the authenticated account. | none | none | +| `list_jobs` | List recent jobs. | none | `limit`, `cursor`, `status` | +| `get_job` | Read job status, phase, scheduling, billing, and artifact readiness. | `job_id` | none | +| `get_job_events` | Read lifecycle events for scheduling, provisioning, startup, failures, and cancellation. | `job_id` | none | +| `get_job_logs` | Read persisted runtime and workload logs. | `job_id` | `limit`, `cursor` | +| `cancel_job` | Request cancellation of a non-terminal job. | `job_id` | `reason` | +| `list_artifacts` | List managed output artifacts for a job. | `job_id` | none | +| `get_artifact` | Create temporary artifact download information. | `job_id`, `artifact_id` | none | -## Real-Time Job Pattern +Accepted `workload_type` values are `inference`, `training`, `fine_tuning`, and `batch`. The MCP server forwards `fine_tuning` to the REST API as `fine-tuning`. Accepted `routing_mode` values are `cost`, `speed`, and `balanced`. -Use `upload_job_input` for files, `submit_job` to start work, `get_job_events` -while the job is queued or starting, `get_job_logs` once workload output exists, -then `list_artifacts` after completion to retrieve saved files. +### Tool Details + +#### `estimate_job` + +Returns classification, route status, capacity source, estimated cost range, availability, and screening details when returned by the API. An estimate is not a reservation and does not guarantee immediate startup. + +Common errors: missing `workload_type`, invalid enum value, authentication failure, forbidden scope, invalid request, upstream API error. + +```json +{ + "workload_type": "inference", + "model_size": 7, + "image": "pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime", + "command": ["python", "infer.py"], + "routing_mode": "balanced", + "notes": "single model inference run" +} +``` + +#### `submit_job` + +Creates an asynchronous job. `command` is preferably an array of strings. `env` must be an object with string values and is forwarded as REST `environment`. `input_files` and `script_files` accept arrays of `{ "input_id": "..." }`; string IDs are normalized for compatibility. The current REST implementation supports one uploaded script reference. + +Expected response includes `job_id`, `status`, `queued_at` or `submitted_at`, routing fields, input/script details, and artifact contract fields when returned by the API. + +Common errors: missing `name`, `image`, or `workload_type`; invalid workload type; command or args too long; invalid environment values; missing or incomplete input IDs; insufficient funds; unavailable capacity; maintenance; authentication or scope failures. + +```json +{ + "name": "transcribe-audio", + "workload_type": "inference", + "image": "python:3.11-slim", + "command": ["python", "/workspace/scripts/transcribe.py", "/workspace/inputs/audio.ogg", "/workspace/artifacts/transcript.txt"], + "script_files": [{ "input_id": "inp_script123" }], + "input_files": [{ "input_id": "inp_audio123" }], + "expected_artifacts": ["/workspace/artifacts/transcript.txt"], + "routing_mode": "balanced", + "metadata": { + "request_id": "req_123" + } +} +``` + +#### `upload_job_input` + +Creates a signed upload slot. It does not upload file bytes by itself. Upload the bytes to `upload.upload_url` using `upload.method`, then complete the upload with `upload.complete_url` and the returned `upload.token`. + +`kind` is an arbitrary string accepted by the API. Use `input` for normal input files and `script` for scripts by convention. Script uploads mount under `/workspace/scripts/`; input uploads mount under `/workspace/inputs/`. + +Expected response: ```json { - "command": ["python", "-c", "import os; exec(os.environ['CODE'])"], - "environment": { - "CODE": "import os, json\nos.makedirs('/workspace/artifacts', exist_ok=True)\nwith open('/workspace/artifacts/output.json','w') as f:\n json.dump({'status':'ok'}, f)" + "upload": { + "input_id": "inp_123", + "filename": "transcribe.py", + "method": "PUT", + "upload_url": "https://signed-upload.example", + "token": "upload_token", + "expires_at": "2026-06-11T12:15:00Z", + "complete_url": "https://api.junglegrid.dev/v1/job-inputs/inp_123/complete" } } ``` -The combined command args limit is 4096 characters. For larger scripts, upload -the script with `upload_job_input` using `kind: "script"`, pass its `input_id` -as `script_files: [{"input_id":"..."}]`, and invoke -`/workspace/scripts/` from `command`. +Common errors: missing filename, invalid filename, file too large, upload storage unavailable, authentication or scope failure. + +#### `list_job_inputs` + +Returns uploaded inputs with `input_id`, `filename`, `content_type`, `size_bytes`, `kind`, `status`, `ready`, `mount_path`, and timestamps when available. + +#### `list_jobs` + +Returns `jobs`, `limit`, `next_cursor`, and `has_more`. `limit` is capped by the API. `status` is a free-form filter string passed to the API; do not assume the MCP schema restricts it to a fixed enum. + +#### `get_job` + +Returns the current job status and details. Status, execution phase, lifecycle events, runtime details, and workload logs are separate surfaces. + +Important response fields include `status`, `phase`, `execution_phase`, `status_message`, `status_reason`, `phase_started_at`, `phase_last_updated_at`, `wait_duration_seconds`, `delayed_start`, `delay_reason`, `scheduling`, `startup_diagnostics`, `provider`, `artifacts_ready`, `failure`, `input_files`, `script_file`, and `artifact_contract` when present. + +#### `get_job_events` + +Returns lifecycle events before and during execution. Events may exist before workload logs begin. Events include IDs, types, phases, titles, messages, source, level, timestamps, sequence, and a generated timestamp. + +Use events to diagnose queueing, route selection, scheduling, provider provisioning, input preparation, startup, retries, failures, and cancellation. + +#### `get_job_logs` + +Returns stored log entries with `items`, `next_cursor`, `has_more`, `failure_highlight`, and `usage_hint` when available. Entries include `entry_id`, `source`, `category`, `stream`, `message`, `truncated`, and `created_at` when returned by the API. + +Logs can be empty while a job is queued, scheduling, provisioning, or preparing. Call `get_job_events` when logs are empty but the job is not terminal. This MCP tool fetches persisted logs; it does not provide true streaming. + +#### `cancel_job` + +Requests cancellation for a pending, queued, assigned, starting, or running job. Completed, failed, rejected, or already cancelled jobs return a conflict from the API. + +Expected response includes `job_id`, `status`, and `status_reason` when returned by the API. Cancellation may trigger managed teardown, but do not assume immediate infrastructure shutdown. + +#### `list_artifacts` + +Returns managed artifacts for a job. Artifacts include `artifact_id`, `job_id`, `filename`, `content_type`, `size_bytes`, `status`, `ready`, and timestamps when returned by the API. Failed jobs may have no artifacts or partial artifacts. + +#### `get_artifact` + +Creates temporary download information for one artifact. The API returns artifact metadata, a signed URL, and `expires_at`. Treat the URL as a secret. + +Common errors: artifact not found, artifact not ready, artifact storage unavailable, forbidden job, authentication failure. + +## Production Workflows + +### Simple Job + +1. Estimate: + +```json +{ + "workload_type": "batch", + "image": "python:3.11-slim", + "command": ["python", "-c", "from pathlib import Path; Path('/workspace/artifacts/output.txt').write_text('done')"], + "routing_mode": "balanced" +} +``` + +1. Submit: + +```json +{ + "name": "simple-artifact-job", + "workload_type": "batch", + "image": "python:3.11-slim", + "command": ["python", "-c", "from pathlib import Path; Path('/workspace/artifacts/output.txt').write_text('done')"], + "expected_artifacts": ["/workspace/artifacts/output.txt"], + "routing_mode": "balanced" +} +``` -For file-based workloads such as transcription: +1. Monitor: + +```json +{ "job_id": "job_123" } +``` + +Call `get_job`, `get_job_events`, and `get_job_logs` with the same `job_id` until the status is terminal. + +1. Retrieve: + +```json +{ "job_id": "job_123" } +``` + +Call `list_artifacts`, then: ```json { - "name": "audio-transcription", + "job_id": "job_123", + "artifact_id": "art_123" +} +``` + +### File-Backed Job + +1. Create upload slots: + +```json +{ + "filename": "transcribe.py", + "content_type": "text/x-python", + "kind": "script" +} +``` + +```json +{ + "filename": "audio.ogg", + "content_type": "audio/ogg", + "kind": "input" +} +``` + +1. Upload each file to the returned signed `upload_url`, then complete it: + +```sh +curl -X PUT "$UPLOAD_URL" \ + -H "Content-Type: text/x-python" \ + --data-binary @transcribe.py + +curl -X POST "$COMPLETE_URL" \ + -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "token": "upload_token", + "filename": "transcribe.py", + "content_type": "text/x-python", + "size_bytes": 1234, + "etag": "optional-etag" + }' +``` + +1. Submit with input IDs: + +```json +{ + "name": "file-backed-transcription", "workload_type": "inference", "image": "python:3.11-slim", "command": ["python", "/workspace/scripts/transcribe.py", "/workspace/inputs/audio.ogg", "/workspace/artifacts/transcript.txt"], @@ -163,61 +399,72 @@ For file-based workloads such as transcription: } ``` -For managed jobs, Jungle Grid automatically creates `/workspace/artifacts` and -uploads any regular files written there. Users do not need to create signed -upload URLs or call artifact completion endpoints manually. +1. Monitor with `get_job_events`, `get_job`, and `get_job_logs`. -`estimate_job` can return `screening.can_submit: true` without confirmed -immediate worker pickup. Check `capacity_status` for whether capacity is -`available`, `limited`, `unavailable`, or `unknown`. After submission, `get_job` -returns `execution_phase`, stable `phase_started_at`, later -`phase_last_updated_at`, `scheduling`, and `delayed_start`. A delayed start is -phase-specific: waiting for compatible capacity is different from managed -runtime preparation after a container is already running. A supported estimate -does not guarantee immediate or successful runtime startup; call -`get_job_events` when workload logs are empty but the job is still scheduling or -preparing. +1. Retrieve `/workspace/artifacts/transcript.txt` with `list_artifacts` and `get_artifact`. -The deprecated tool argument `workload` is accepted as a temporary alias for -`workload_type`, and legacy string file IDs are normalized where possible. New -requests should use `workload_type`, `command` arrays, and `{ "input_id": "..." }` -file references. +## Error Shape -## Local Development +REST MCP routes return an envelope: + +```json +{ + "ok": false, + "error": { + "code": "INVALID_REQUEST", + "message": "name, image, and workload_type are required" + } +} +``` + +The MCP server converts API errors into tool errors like: + +```text +submit_job failed: INVALID_REQUEST: name, image, and workload_type are required +``` + +Common API codes include `UNAUTHORIZED`, `FORBIDDEN`, `INVALID_REQUEST`, `JOB_INPUT_NOT_FOUND`, `JOB_INPUT_NOT_READY`, `ARTIFACT_NOT_READY`, `NOT_FOUND`, `CONFLICT`, `INSUFFICIENT_FUNDS`, `MAINTENANCE_ACTIVE`, and `INTERNAL_ERROR`. + +## Security + +- Keep API keys and OAuth tokens out of prompts, source control, browser bundles, logs, and issue trackers. +- Prefer host secret stores or local-only MCP config files for `JUNGLE_GRID_API_KEY`. +- Treat signed upload and artifact URLs as temporary bearer secrets. +- Do not print environment variables that contain tokens from workload code. +- Review `submit_job` and `cancel_job` requests before allowing an agent to execute them, because they can spend credits or stop active work. + +## Development ```sh npm install npm run build -JUNGLE_GRID_API_KEY=jg_... node dist/index.js +npm test ``` -Inspect the server with MCP Inspector: +Run stdio from the built package: ```sh -JUNGLE_GRID_API_KEY=jg_... npx @modelcontextprotocol/inspector node dist/index.js +JUNGLE_GRID_API_KEY=jg_placeholder node dist/index.js ``` -## Publishing - -Verify the package before publishing: +Run HTTP locally: ```sh -npm run build -npm pack --dry-run +MCP_TRANSPORT=http PORT=3000 JUNGLEGRID_INTERNAL_SERVICE_TOKEN=service_token_placeholder node dist/index.js ``` -Publish the scoped package publicly: +Inspect with MCP Inspector: ```sh -npm publish --access public +JUNGLE_GRID_API_KEY=jg_placeholder npx @modelcontextprotocol/inspector node dist/index.js ``` -## Troubleshooting +## Full Documentation + +Public Jungle Grid documentation: https://junglegrid.dev/docs + +MCP documentation page: https://junglegrid.dev/docs/mcp + +## License -- `JUNGLE_GRID_API_KEY environment variable is required`: add the key to the - host config `env` block or to the environment that launches the host. -- Tools do not appear: fully quit and reopen the MCP host after editing config. -- Old package version: pin a version in config, for example - `["@jungle-grid/mcp@0.1.0"]`, or clear the npx cache. -- API calls fail: confirm the key is valid and `JUNGLE_GRID_API_URL` points to - the orchestrator you intend to use. +MIT diff --git a/examples/prompts.md b/examples/prompts.md index b47c1ce..c5c19ff 100644 --- a/examples/prompts.md +++ b/examples/prompts.md @@ -6,7 +6,7 @@ MCP-compatible agents that have the `junglegrid` server enabled. ## Estimate a Job Before Submission ```text -Estimate the cost of running a batch job with image nvidia/cuda:12.2.0-base-ubuntu22.04 and command ["bash","-lc","python train.py --epochs 3"]. Optimise for cost and prefer us-east if possible. +Estimate the cost of running a batch job with image nvidia/cuda:12.2.0-base-ubuntu22.04 and command ["bash","-lc","python train.py --epochs 3"]. Use routing_mode "cost" and explain whether the estimate confirms immediate capacity or only supported/provisionable capacity. ``` ## Submit a Managed Batch Job @@ -18,25 +18,25 @@ Submit a batch job named "mnist-train" using image pytorch/pytorch:2.2.0-cuda12. ## Check Job Status ```text -Check the status of Jungle Grid job job_123. Summarise whether it is still queued, running, or finished and include any scheduling reason if available. +Check the status of Jungle Grid job job_123. Summarise the current status, execution phase, phase timing, delayed-start reason, scheduling details, and artifact readiness if available. ``` -## Stream Live Logs +## Inspect Lifecycle Events ```text -Stream live logs for Jungle Grid job job_123 for up to 180 seconds. Show stdout, stderr, and tell me whether the process exited cleanly. +Fetch lifecycle events for Jungle Grid job job_123. Explain what happened before workload logs began, including queueing, route selection, scheduling, provisioning, startup, retry, failure, or cancellation events. ``` -## Retrieve Final Logs +## Retrieve Workload Logs ```text -Fetch the final runtime logs for Jungle Grid job job_123. If stdout or stderr is unavailable, explain why from the runtime metadata. +Fetch persisted workload logs for Jungle Grid job job_123. If no workload logs are available yet, also fetch lifecycle events and explain whether the job is still queued, scheduling, provisioning, or preparing runtime. ``` ## Failure Analysis ```text -Inspect Jungle Grid job job_123. Get the latest status, fetch runtime logs, and explain the most likely reason the workload failed. +Inspect Jungle Grid job job_123. Get the latest status, lifecycle events, persisted logs, and artifact list. Explain the most likely failure stage and whether there are retry, cancellation, or artifact clues. ``` ## Inspect Managed Artifacts @@ -50,3 +50,9 @@ List managed artifacts for Jungle Grid job job_123. If files are available, get ```text Submit a batch job with image python:3.11-slim and command ["python","-c","import os; print('hello from jungle grid')"]. Use the most cost-effective routing that is currently available. ``` + +## Submit a File-Backed Job + +```text +Create upload slots for transcribe.py as a script and audio.ogg as an input. After I upload and complete both files, submit an inference job that runs python /workspace/scripts/transcribe.py /workspace/inputs/audio.ogg /workspace/artifacts/transcript.txt, then monitor events, status, logs, and retrieve the transcript artifact. +```