{
  "markdown": "# CodeQR Remote MCP Server\n\nRemote MCP server for [CodeQR](https://codeqr.io) with OAuth 2.0 authentication. Compatible with ChatGPT, OpenAI Agents SDK, Grok (xAI), and any MCP client supporting Streamable HTTP transport.\n\n## How It Works\n\n```\n┌──────────┐      ┌─────────────────────┐      ┌──────────┐\n│  ChatGPT │      │  CodeQR MCP Remote  │      │ CodeQR   │\n│  or any  │─────▶│                     │─────▶│ API      │\n│  MCP     │◀─────│  OAuth 2.0 + MCP    │◀─────│          │\n│  client  │      │  Streamable HTTP    │      │          │\n└──────────┘      └─────────────────────┘      └──────────┘\n```\n\n1. Client discovers auth endpoints via `/.well-known/oauth-protected-resource`\n2. Client registers dynamically via `POST /oauth/register`\n3. `GET /oauth/authorize` sends the user to CodeQR, where they log in, choose\n   which project to grant access to, and approve — no API key is ever handled\n4. CodeQR returns them to `GET /oauth/callback`, which trades the code for an\n   access + refresh token pair\n5. Client exchanges authorization code for access token (PKCE)\n6. Client sends MCP tool calls with Bearer token to `POST /mcp`\n\nThe CodeQR access token lasts 7 days and is renewed transparently, so the\nsession stays valid for the 120-day life of the refresh token.\n\n## Quick Start\n\n```bash\n# Install dependencies\nnpm install\n\n# Set environment variables\ncp .env.example .env\n# Edit .env with your SERVER_URL\n\n# Development\nnpm run dev\n\n# Production\nnpm run build\nnpm start\n```\n\n## Deploy\n\n### Vercel (Recommended)\n\nThe project is configured for Vercel serverless functions:\n\n```bash\n# Install Vercel CLI\nnpm i -g vercel\n\n# Deploy\nvercel\n\n# Set environment variables in Vercel dashboard:\n# - SERVER_URL — Your public server URL (e.g., https://mcp.codeqr.io)\n# - UPSTASH_REDIS_REST_URL — From your Upstash Redis database (REST API)\n# - UPSTASH_REDIS_REST_TOKEN — From your Upstash Redis database\n# - CODEQR_OAUTH_CLIENT_ID — client_id of the OAuth app registered in CodeQR\n# - CODEQR_OAUTH_CLIENT_SECRET — its client_secret\n# - CODEQR_APP_URL — Dashboard origin (default: https://app.codeqr.io)\n# - STAINLESS_API_KEY — Optional Stainless API key\n# - LOG_LEVEL — Log level (default: info)\n```\n\nThe app will be available at `https://your-project.vercel.app`. All routes are handled by the serverless function at `api/server.ts`.\n\n**OAuth storage:** Set **Upstash Redis** (`UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`) so authorization codes, access tokens, and registered clients persist across serverless invocations. If these variables are omitted, the server falls back to an in-memory store (fine for local development only).\n\n### Docker\n\n```bash\ndocker build -t codeqr-mcp-remote .\ndocker run -p 3000:3000 -e SERVER_URL=https://mcp.codeqr.io codeqr-mcp-remote\n```\n\n### Railway / Render / Fly.io\n\nSet environment variables:\n- `SERVER_URL` — Your public server URL (e.g., `https://mcp.codeqr.io`)\n- `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` — Recommended for multi-instance or restarts\n- `PORT` — Port (usually set automatically by the platform)\n\n## Connect to ChatGPT\n\n1. Deploy this server to a public URL\n2. In ChatGPT, go to **Settings > Advanced > Developer Mode**\n3. Go to the **Connectors** tab\n4. Click **Add Connector**\n5. Enter your server URL (e.g., `https://mcp.codeqr.io/mcp`)\n6. ChatGPT will auto-discover the OAuth endpoints and prompt you to authorize\n\n## Connect to OpenAI Agents SDK\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI()\n\nresponse = client.responses.create(\n    model=\"gpt-4o\",\n    tools=[{\n        \"type\": \"mcp\",\n        \"server_label\": \"codeqr\",\n        \"server_url\": \"https://mcp.codeqr.io/mcp\",\n        \"require_approval\": \"never\",\n    }],\n    input=\"Create a short link for https://example.com\",\n)\n```\n\n## Available Tools\n\n| Tool | Description |\n|------|-------------|\n| `create_link` | Create a shortened link |\n| `list_links` | List all short links |\n| `get_link_info` | Get link details |\n| `update_link` | Update a link |\n| `delete_link` | Delete a link |\n| `create_qrcode` | Create a dynamic QR code encoding a URL, Wi-Fi credentials, a contact card, WhatsApp, email, SMS, a phone number, text or a crypto request |\n| `list_qrcodes` | List all QR codes |\n| `update_qrcode` | Change where an existing QR code points, without reprinting it |\n| `delete_qrcode` | Delete a QR code; printed copies stop resolving |\n| `get_analytics` | Query click analytics |\n| `list_domains` | List custom domains |\n| `list_tags` | List tags |\n| `create_tag` | Create a tag |\n| `get_workspace` | Read the authorized workspace: name, slug and plan |\n\nConversion event tools are not offered. `track_lead` and `track_sale` need the\n`conversions.write` scope, which CodeQR grants to workspace owners only, and\nrequesting it makes CodeQR reject the whole authorization for everyone else.\nThe per-link `trackConversion` toggle is a `links.write` field and IS exposed\non `create_link`/`update_link` (plans that include conversion tracking only).\n\nSmart rules are exposed on `create_link`/`update_link` as `rules`: conditional\nrouting by any of the twelve attributes the API implements, and traffic\nsplitting across 2-4 destinations, which is how an A/B test is expressed — one\nrule with no condition and a `split`. Business plan and above; below it the API\nrejects the whole call. Four of the field's invariants cannot be stated in JSON\nSchema (weights totalling 100, `url` xor `split`, the all-or-nothing condition,\nthe unconditional rule coming last), so they are checked in\n`src/smart-rules.ts` before the request is sent — which saves a round-trip and\nanswers in a sentence, rather than the serialized error body the SDK surfaces.\n\nThe trap worth knowing: `value` is compared whole and case-insensitively\nagainst what the request carries, which for three attributes is narrower than\nthe name suggests. `device` is the operating system (`iOS`, `Android`,\n`Windows`, `Mac OS`, `Linux` — never `mobile`), `language` a two-letter code,\n`referrer` a bare domain. A wrong value is not an error anywhere: the API\naccepts any string and the rule silently never matches.\n\n## API Endpoints\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| GET | `/health` | No | Health check |\n| GET | `/.well-known/oauth-protected-resource` | No | OAuth resource metadata (RFC 9728) |\n| GET | `/.well-known/oauth-authorization-server` | No | OAuth server metadata (RFC 8414) |\n| POST | `/oauth/register` | No | Dynamic client registration (RFC 7591) |\n| GET | `/oauth/authorize` | No | Redirects the user to CodeQR to approve |\n| GET | `/oauth/callback` | No | Return leg from CodeQR |\n| POST | `/oauth/token` | No | Token exchange |\n| POST | `/mcp` | Bearer | MCP Streamable HTTP endpoint |\n\n## Architecture\n\n```\nsrc/\n├── index.ts              # Express app & server startup\n├── config.ts             # Environment configuration\n├── oauth/\n│   ├── store.ts          # OAuth state: Upstash Redis or in-memory fallback\n│   └── pkce.ts           # PKCE S256 verification\n├── middleware/\n│   └── auth.ts           # Bearer token validation middleware\n└── routes/\n    ├── well-known.ts     # OAuth discovery metadata endpoints\n    ├── oauth.ts          # Authorization & token endpoints\n    └── mcp.ts            # MCP tool definitions & handlers\n```\n\n## Production Considerations\n\n- **OAuth persistence:** Configure **Upstash Redis** (see `.env.example`) for serverless and multi-instance deployments. Without it, the in-memory store is used (single process only).\n- **Add rate limiting** to the OAuth and MCP endpoints\n- **Add HTTPS** (usually handled by your reverse proxy / platform)\n- **Add monitoring** (the `/health` endpoint is ready for probes)\n- **Consider token rotation** for long-lived sessions\n\n## Publishing to the MCP Registry\n\nThis server is listed in the official registry as `io.codeqr/codeqr`. The registry\nstores only the metadata in `server.json` — never the code.\n\nPublishing is authorized by DNS: a TXT record on the **apex** of `codeqr.io` holds\nthe public half of an Ed25519 key pair. Apex, not a selector — MCP DNS auth follows\nSPF-style placement, and a record under `_mcp-auth.` fails with a generic signature\nerror that does not name the cause.\n\n```bash\n# 1. Bump the version in package.json, src/config.ts and server.json together.\n#    `yarn test` fails if they drift — the registry refuses to republish a\n#    version it already has.\n#\n#    Then bump it in the CodeQR app too: the Server Card at\n#    app/.well-known/mcp/server-card.json/route.ts advertises this server's\n#    version, and no test can reach across repos to check. It already drifted\n#    once, to a release that never existed.\n\n# 2. Authenticate with the private key (kept outside this repo).\nPRIVATE_KEY=\"$(openssl pkey -in /path/to/codeqr-io.pem -noout -text | grep -A3 \"priv:\" | tail -n +2 | tr -d ' :\\n')\"\nmcp-publisher login dns --domain codeqr.io --private-key \"$PRIVATE_KEY\"\n\n# 3. Publish, then confirm the entry is live.\nmcp-publisher publish\ncurl \"https://registry.modelcontextprotocol.io/v0/servers?search=codeqr\"\n```\n\nIf the key is ever rotated, **remove the old TXT record** — a stale one is tried\nfirst and makes verification fail.\n\n### Directory listings\n\nAggregators crawl the ecosystem and list servers whether or not anyone claims them,\nso an unclaimed entry still exists — as a bot's guess at what this server does.\nClaiming replaces the guess and unlocks editing the name and description a reader\nsees. None of this is done by merging a file; each one is a one-time action on the\naggregator's own site.\n\n| Directory | How ownership is claimed | Status |\n|-----------|--------------------------|--------|\n| [Official MCP Registry](https://registry.modelcontextprotocol.io) | `mcp-publisher` + DNS, as above | listed |\n| [Glama](https://glama.ai) | `glama.json` in this repo, then run the claim flow once from an account listed in `maintainers` | file in repo; claim pending |\n| [Smithery](https://smithery.ai) | `smithery mcp publish https://mcp.codeqr.io/mcp -n <org>/<name>` | pending |\n| [PulseMCP](https://pulsemcp.com) | hand-reviewed submission on their site | pending |\n| [mcp.so](https://mcp.so) | submission form / GitHub issue | pending |\n\nThe GitHub OAuth route Glama also offers only associates repos under a **personal**\naccount, which this repo is not — it belongs to the `codeqr-io` org. Hence the file.\n\n## License\n\nMIT — [CodeQR](https://codeqr.io)\n",
  "bytes": 10442,
  "sha": "869ebd43907fb9962df249aa0250d8aecc1628fa3126ca211059431f8a058d3c",
  "repo_slug": "codeqr-io/codeqr-mcp-remote",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_codeqr_codeqr_c4402bd4/readme"
}