diff --git a/README.md b/README.md index a56d389..f8dcc8c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ npx mcp-runtime list # list all configured servers npx mcp-runtime list vercel --schema # show tool signatures + schemas npx mcp-runtime call linear.searchIssues owner=ENG status=InProgress npx mcp-runtime call signoz.query --tail-log # print the tail of returned log files + +# local scripts mirroring the Sweetistics workflow +pnpm mcp:list # alias for mcp-runtime list +pnpm mcp:call chrome-devtools.getTabs --tail-log ``` Common flags: diff --git a/config/mcp_servers.json b/config/mcp_servers.json new file mode 100644 index 0000000..4d2cfa4 --- /dev/null +++ b/config/mcp_servers.json @@ -0,0 +1,84 @@ +[ + { + "name": "chrome-devtools", + "description": "Chrome DevTools protocol bridge for driving local tabs during debugging or automation.", + "command": [ + "bash", + "scripts/mcp_stdio_wrapper.sh", + "env", + "npm_config_loglevel=error", + "npx", + "-y", + "chrome-devtools-mcp@latest" + ] + }, + { + "name": "context7", + "description": "Context7 MCP for React documentation lookup.", + "command": "https://mcp.context7.com/mcp" + }, + { + "name": "linear", + "description": "Hosted Linear MCP; exposes issue search, create, and workflow tooling.", + "command": "https://mcp.linear.app/mcp", + "headers": { + "Authorization": "Bearer ${LINEAR_API_KEY}" + } + }, + { + "name": "next-devtools", + "description": "Next.js dev server introspection (project metadata, logs, page insights).", + "command": "http://localhost:3000/_next/mcp" + }, + { + "name": "playwright", + "description": "Playwright MCP server for accessibility-driven automation.", + "command": [ + "bash", + "scripts/mcp_stdio_wrapper.sh", + "env", + "npm_config_loglevel=error", + "npx", + "-y", + "@playwright/mcp@latest" + ] + }, + { + "name": "vercel", + "description": "Vercel MCP (requires OAuth).", + "command": "https://mcp.vercel.com", + "auth": "oauth", + "token_cache_dir": "~/.cache/mcp-runtime/vercel" + }, + { + "name": "firecrawl", + "description": "Firecrawl hosted MCP for authenticated scraping and extraction.", + "command": "https://mcp.firecrawl.dev/v2/mcp", + "headers": { + "Authorization": "Bearer ${FIRECRAWL_API_KEY}" + } + }, + { + "name": "signoz", + "description": "SigNoz Query MCP server (logs, traces, metrics).", + "command": [ + "bash", + "scripts/mcp_stdio_wrapper.sh", + "env", + "NODE_OPTIONS=--require=./scripts/mcp_signoz_retry_patch.cjs", + "npm_config_loglevel=error", + "npx", + "-y", + "signoz-mcp-server@latest" + ], + "env": { + "SIGNOZ_URL": "${SIGNOZ_URL:-http://localhost:3301}", + "SIGNOZ_TOKEN": "${SIGNOZ_TOKEN:-}" + } + }, + { + "name": "shadcn", + "description": "shadcn/ui registry MCP for browsing component recipes.", + "command": "https://www.shadcn.io/api/mcp" + } +] diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..5668ccf --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,52 @@ +--- +summary: 'Working with configured MCP servers via the mcp-runtime CLI and scripts.' +--- + +# MCP Workflow + +`mcp-runtime` reads server definitions from `config/mcp_servers.json`. Each entry mirrors the Sweetistics setup (headers, stdio wrappers, OAuth hints) so team members can lean on the same command surface area without Python dependencies. + +## Scripts + +``` +pnpm mcp:list [] [--schema] +pnpm mcp:call . [key=value...] [--args '{"foo":"bar"}'] [--tail-log] +``` + +Both scripts forward straight to the TypeScript CLI (`src/cli.ts`), so they support the same flags documented in the [README](../README.md). + +- `pnpm mcp:list` – enumerate all configured servers; pass a specific name to inspect tool signatures. +- `pnpm mcp:list --schema` – dump the full JSON schema for each tool exposed by ``. +- `pnpm mcp:call` – execute a tool using either loose `key=value` pairs or `--args` JSON; append `--tail-log` to follow log files reported by the response. + +## Adding or Updating Servers + +1. Edit `config/mcp_servers.json` (keep the entries sorted alphabetically). +2. Use `${ENV}` or `${ENV:-default}` placeholders for secrets; they are resolved at runtime. +3. Set `auth: "oauth"` when the server requires an OAuth dance – the CLI spins up a local callback server and persists tokens under `~/.mcp-runtime//`. +4. For stdio transports, wrap the command with `scripts/mcp_stdio_wrapper.sh` to inherit repo-relative paths, just like the Sweetistics helper. + +After editing the config, you can validate the entry with: + +``` +pnpm mcp:list +pnpm mcp:call . --args '{"sample":true}' +``` + +## Environment Variables + +The CLI respects the same conventions as the original Python wrapper: + +- `${LINEAR_API_KEY}`, `${FIRECRAWL_API_KEY}`, etc. for hosted servers. +- `$env:VAR` to inject the raw runtime value without fallbacks. +- `env` blocks per entry to provide default values (e.g., SigNoz URLs/tokens). + +## Troubleshooting + +| Issue | Suggested Fix | +| --- | --- | +| OAuth flow never completes | Ensure the browser opened `http://127.0.0.1:/callback`; copy the printed URL manually if not. | +| Stdio command cannot find scripts | Pass `--root ` or run from the project root so relative paths resolve. | +| Tokens are stale | Delete `~/.mcp-runtime//tokens.json` and rerun the command. | + +Refer to [`docs/spec.md`](./spec.md) for deeper architectural notes and future roadmap items. diff --git a/package.json b/package.json index d82c617..9fea1f7 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,17 @@ }, "scripts": { "build": "tsc -p tsconfig.build.json", - "check": "biome check", + "check": "pnpm lint:biome && pnpm lint:oxlint && pnpm typecheck", "lint": "pnpm check", + "lint:biome": "biome check", + "lint:oxlint": "oxlint --type-aware --tsconfig tsconfig.json --max-warnings=0", + "typecheck": "tsgo --project tsconfig.json --noEmit", "test": "vitest run", "clean": "rimraf dist", "dev": "tsc -w -p tsconfig.build.json", - "prepublishOnly": "pnpm check && pnpm test && pnpm build" + "prepublishOnly": "pnpm check && pnpm test && pnpm build", + "mcp:list": "pnpm exec tsx src/cli.ts list", + "mcp:call": "pnpm exec tsx src/cli.ts call" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.10.1", @@ -36,8 +41,12 @@ "@biomejs/biome": "^2.3.3", "@types/express": "^4.17.21", "@types/node": "^22.7.4", + "@typescript/native-preview": "7.0.0-dev.20251104.1", "express": "^4.21.1", + "oxlint": "^1.25.0", + "oxlint-tsgolint": "^0.5.0", "rimraf": "^6.0.1", + "tsx": "^4.16.5", "typescript": "^5.6.3", "vitest": "^1.6.0" }, diff --git a/scripts/mcp_signoz_retry_patch.cjs b/scripts/mcp_signoz_retry_patch.cjs new file mode 100644 index 0000000..46a136e --- /dev/null +++ b/scripts/mcp_signoz_retry_patch.cjs @@ -0,0 +1,9 @@ +// No-op hook retained for compatibility with the Sweetistics configuration. +// Signoz MCP occasionally retries on transient errors; keeping this module +// allows `NODE_OPTIONS=--require=./scripts/mcp_signoz_retry_patch.cjs` to load +// without throwing if teams reuse the same entry. + +module.exports = { + // Export a no-op hook so consumers can keep requiring this patch file safely. + onRetry() {}, +}; diff --git a/scripts/mcp_stdio_wrapper.sh b/scripts/mcp_stdio_wrapper.sh new file mode 100755 index 0000000..208b65c --- /dev/null +++ b/scripts/mcp_stdio_wrapper.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -eq 0 ]]; then + echo "mcp_stdio_wrapper.sh: missing command" >&2 + exit 1 +fi + +exec "$@" diff --git a/tests/runtime-integration.test.ts b/tests/runtime-integration.test.ts index 8aa0c0a..70f11ad 100644 --- a/tests/runtime-integration.test.ts +++ b/tests/runtime-integration.test.ts @@ -12,6 +12,9 @@ import { createRuntime } from "../src/runtime.js"; const app = express(); app.use(express.json()); +app.get("/mcp", (_req, res) => { + res.sendStatus(405); +}); const server = new McpServer({ name: "integration-demo", @@ -42,18 +45,28 @@ server.registerResource( title: "Greeting", description: "Dynamic greeting resource", }, - async (uri, { name }) => ({ - contents: [ - { - uri: uri.href, - text: `Hello, ${name}!`, - }, - ], - }), + async (uri, { name }) => { + const normalizedName = + typeof name === "string" + ? name + : Array.isArray(name) + ? name.join(", ") + : "friend"; + + return { + contents: [ + { + uri: uri.href, + text: `Hello, ${normalizedName}!`, + }, + ], + }; + }, ); app.post("/mcp", async (req, res) => { const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, enableJsonResponse: true, });