CodeQR Remote MCP Server
Remote MCP server for CodeQR with OAuth 2.0 authentication. Compatible with ChatGPT, OpenAI Agents SDK, Grok (xAI), and any MCP client supporting Streamable HTTP transport.
How It Works
ββββββββββββ βββββββββββββββββββββββ ββββββββββββ
β ChatGPT β β CodeQR MCP Remote β β CodeQR β
β or any βββββββΆβ βββββββΆβ API β
β MCP ββββββββ OAuth 2.0 + MCP ββββββββ β
β client β β Streamable HTTP β β β
ββββββββββββ βββββββββββββββββββββββ ββββββββββββ
- Client discovers auth endpoints via
/.well-known/oauth-protected-resource
- Client registers dynamically via
POST /oauth/register
GET /oauth/authorize shows this server's consent screen β the app's
self-declared name, marked unverified unless its exact redirect URI is
listed in src/oauth/verified-clients.ts, and the host that redirect URI
points at. On approval the user goes to CodeQR, where they log in, choose which
project to grant access to, and approve β no API key is ever handled.
The callback only completes in the browser that approved.
- CodeQR returns them to
GET /oauth/callback, which trades the code for an
access + refresh token pair
- Client exchanges authorization code for access token (PKCE)
- Client sends MCP tool calls with Bearer token to
POST /mcp
The CodeQR access token lasts 7 days and is renewed transparently. Each
renewal gives the session another 120 days, so a connection in use does not
expire on a fixed date; an idle one ends 120 days after its last renewal, and
none outlives one year from when it was made (the expires_in the client is
given).
Quick Start
npm install
cp .env.example .env
npm run dev
npm run build
npm start
Deploy
Vercel (Recommended)
The project is configured for Vercel serverless functions:
The app will be available at https://your-project.vercel.app. All routes are handled by the serverless function at api/server.ts.
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).
Docker
docker build -t codeqr-mcp-remote .
docker run -p 3000:3000 -e SERVER_URL=https://mcp.codeqr.io codeqr-mcp-remote
Railway / Render / Fly.io
Set environment variables:
SERVER_URL β Your public server URL (e.g., https://mcp.codeqr.io)
UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN β Recommended for multi-instance or restarts
PORT β Port (usually set automatically by the platform)
Connect to ChatGPT
- Deploy this server to a public URL
- In ChatGPT, go to Settings > Advanced > Developer Mode
- Go to the Connectors tab
- Click Add Connector
- Enter your server URL (e.g.,
https://mcp.codeqr.io/mcp)
- ChatGPT will auto-discover the OAuth endpoints and prompt you to authorize
Connect to OpenAI Agents SDK
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-4o",
tools=[{
"type": "mcp",
"server_label": "codeqr",
"server_url": "https://mcp.codeqr.io/mcp",
"require_approval": "never",
}],
input="Create a short link for https://example.com",
)
| Tool | Description |
|---|
create_link | Create a shortened link |
list_links | List all short links |
get_link_info | Get link details |
update_link | Update a link |
delete_link | Delete a link |
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 |
list_qrcodes | List all QR codes |
update_qrcode | Change where an existing QR code points, without reprinting it |
delete_qrcode | Delete a QR code; printed copies stop resolving |
get_analytics | Query click analytics |
list_domains | List custom domains |
list_tags | List tags |
create_tag | Create a tag |
get_workspace | Read the authorized workspace: name, slug and plan |
Conversion event tools are not offered. track_lead and track_sale need the
conversions.write scope, which CodeQR grants to workspace owners only, and
requesting it makes CodeQR reject the whole authorization for everyone else.
The per-link trackConversion toggle is a links.write field and IS exposed
on create_link/update_link (plans that include conversion tracking only).
Smart rules are exposed on create_link/update_link as rules: conditional
routing by any of the twelve attributes the API implements, and traffic
splitting across 2-4 destinations, which is how an A/B test is expressed β one
rule with no condition and a split. Where the workspace doesn't have smart
rules enabled, the API rejects the whole call β the create or update fails
outright, so no link or QR code is created without the rules it asked for β
and the server doesn't forward that rejection as-is: it reports that smart
rules aren't enabled on this workspace, with a link to details.
src/plan-limit-message.ts recognizes plan wording it knows: a capability
gate like this one becomes "isn't enabled on this workspace", a quota becomes
"has reached its limit of N β¦"; anything it doesn't recognize passes through
unchanged. Four of the field's invariants cannot be stated in JSON Schema
(weights totalling 100, url xor split, the all-or-nothing condition, the
unconditional rule coming last), so they are checked in src/smart-rules.ts
before the request is sent β which saves a round-trip and answers in a
sentence, rather than the serialized error body the SDK surfaces.
The trap worth knowing: value is compared whole and case-insensitively
against what the request carries, which for three attributes is narrower than
the name suggests. device is the operating system (iOS, Android,
Windows, Mac OS, Linux β never mobile), language a two-letter code,
referrer a bare domain. A wrong value is not an error anywhere: the API
accepts any string and the rule silently never matches.
create_qrcode/update_qrcode also take title and walletPass. title
sets the QR code's display title and is printed in large type on the
Google/Apple Wallet pass saved from the code (replacing "QR Code"). Within
walletPass, name and logo override the workspace name and logo shown on
that pass, and barcodeText sets the text shown under the QR code, e.g. a
member name or card number; when it is absent, CodeQR sends no text for that
line β the title is never repeated there. Custom branding is only available
on plans that include it, and never during a trial: a new non-null
walletPass is rejected with 403 on any other plan or during a trial, while
null (clearing it) is always accepted. Outside those plans, or during a
trial, a stored value is ignored when the pass is built, so passes fall back
to the workspace name and logo. logo is
an https URL: the API downloads it and stores its own copy, so the response
returns that copy's URL, not the one sent. barcodeText is 1-40 characters
after trimming. On update_qrcode, an object replaces the stored value as a
whole (a field left out of it is cleared, not merged) β send name, logo
and barcodeText together to keep all three; null clears the override and
reverts to the workspace branding, the same way rules: null ends a running
smart-rules test.
API Endpoints
| Method | Path | Auth | Description |
|---|
| GET | /health | No | Health check |
| GET | /.well-known/oauth-protected-resource | No | OAuth resource metadata (RFC 9728) |
| GET | /.well-known/oauth-authorization-server | No | OAuth server metadata (RFC 8414) |
| POST | /oauth/register | No | Dynamic client registration (RFC 7591) |
| GET | /oauth/authorize | No | Consent screen: which app, and where access is sent |
| POST | /oauth/authorize | No | Consent decision; on approval redirects to CodeQR |
| GET | /oauth/callback | No | Return leg from CodeQR |
| POST | /oauth/token | No | Token exchange |
| POST | /mcp | Bearer | MCP Streamable HTTP endpoint |
Architecture
src/
βββ index.ts # Express app & server startup
βββ config.ts # Environment configuration
βββ oauth/
β βββ store.ts # OAuth state: Upstash Redis or in-memory fallback
β βββ pkce.ts # PKCE S256 verification
βββ middleware/
β βββ auth.ts # Bearer token validation middleware
βββ routes/
βββ well-known.ts # OAuth discovery metadata endpoints
βββ oauth.ts # Authorization & token endpoints
βββ mcp.ts # MCP tool definitions & handlers
Production Considerations
- 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).
- Add rate limiting to the OAuth and MCP endpoints
- Add HTTPS (usually handled by your reverse proxy / platform)
- Add monitoring (the
/health endpoint is ready for probes)
- Consider token rotation for long-lived sessions
Publishing to the MCP Registry
This server is listed in the official registry as io.codeqr/codeqr. The registry
stores only the metadata in server.json β never the code.
Publishing is authorized by DNS: a TXT record on the apex of codeqr.io holds
the public half of an Ed25519 key pair. Apex, not a selector β MCP DNS auth follows
SPF-style placement, and a record under _mcp-auth. fails with a generic signature
error that does not name the cause.
PRIVATE_KEY="$(openssl pkey -in /path/to/codeqr-io.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')"
mcp-publisher login dns --domain codeqr.io --private-key "$PRIVATE_KEY"
mcp-publisher publish
curl "https://registry.modelcontextprotocol.io/v0/servers?search=codeqr"
If the key is ever rotated, remove the old TXT record β a stale one is tried
first and makes verification fail.
Directory listings
Aggregators crawl the ecosystem and list servers whether or not anyone claims them,
so an unclaimed entry still exists β as a bot's guess at what this server does.
Claiming replaces the guess and unlocks editing the name and description a reader
sees. None of this is done by merging a file; each one is a one-time action on the
aggregator's own site.
| Directory | How ownership is claimed | Status |
|---|
| Official MCP Registry | mcp-publisher + DNS, as above | listed |
| Glama | glama.json in this repo, then run the claim flow once from an account listed in maintainers | file in repo; claim pending |
| Smithery | smithery mcp publish https://mcp.codeqr.io/mcp -n <org>/<name> | pending |
| PulseMCP | hand-reviewed submission on their site | pending |
| mcp.so | submission form / GitHub issue | pending |
The GitHub OAuth route Glama also offers only associates repos under a personal
account, which this repo is not β it belongs to the codeqr-io org. Hence the file.
License
MIT β CodeQR