I have ~40 Claude Code skills in ~/.claude/skills/ — business context, runbooks, design systems, decision memos. They work perfectly on Claude Code Desktop but are invisible to claude.ai web and mobile, which obviously can’t read my filesystem. The fix turns out to be a tiny Node MCP server that loads the skill markdown files at startup and exposes two tools ( list_skills , load_skill ) to any MCP client that connects. claude.ai’s custom connector is one such client. Stand up the server, register it as a connector, your skills work on web and mobile. Stack: TypeScript + Express + u/modelcontextprotocol /sdk , deployed to Render free tier, OAuth-gated. ~90 minutes end to end. Cost so far: zero. Two gotchas that ate hours when I first tried this in March: Use the /mcp path, not /sse . The SSE transport got deprecated in the MCP spec. Every older blog post and example repo points at /sse and will silently fail to handshake against claude.ai today. OAuth is required, and it actually works now. Three OAuth bugs that broke things in March ( #75 , #85 , #109 on anthropics/claude-ai-mcp ) all closed on 2026-03-29. The flow is reliable since. There’s still no field for a static bearer token in the connector UI ( #10 is open), so OAuth is the only path — but pre-register one static client via env vars and claude.ai has fixed credentials to paste into Advanced settings. No user-consent UI to babysit. Once it’s running, paste a short blurb into your claude.ai Personal Preferences and Claude will check list_skills automatically at the start of every conversation — so you don’t have to remember to mention “I have a skill for this” each time. I wrote up the full setup — architecture, code, OAuth env-var generation, the auto-sync script (Windows Task Scheduler entry so local edits propagate to mobile within ~2 hours of saving), security model (CT-log threat, why OAuth not URL obscurity), and a ready-to-go first prompt that exercises the connector. DM me if you want the link — keeping it off the public post for hygiene reasons. TL;DR: Clone a small Node repo, drop your skills in src/skills/ , deploy to Render free tier, generate OAuth credentials, paste them into claude.ai’s custom connector Advanced settings. Skills work on web and mobile, edits propagate automatically. ~90 minutes if you don’t hit the two gotchas above. The problem Claude Code skills live as markdown files in ~/.claude/skills/<name>/SKILL.md on whatever machine you’ve set them up on. They work perfectly in Claude Code Desktop but are invisible to claude.ai web and mobile — those clients can’t read your filesystem. The fix A tiny Node MCP server that loads your skill markdown files at startup and exposes two tools (list_skills, load_skill) to any MCP client. claude.ai’s custom connector is one such client. Stand up the server, register it as a connector, your skills work on web and mobile. ~150 lines of TypeScript, ~90 minutes from clone to mobile, $0/mo on Render’s free tier. Stack TypeScript + Express + u/modelcontextprotocol/sdk + zod Dockerfile that bundles skills into the image at build time Render free tier (Railway/Fly/Cloudflare Workers all work too) Two gotchas that ate hours when I first tried this in March 2026 Use the /mcp path, not /sse. The SSE transport was deprecated in the MCP spec (March 2025). Every older blog post and example repo still points at /sse and will silently fail to handshake against claude.ai today. Wire your server with StreamableHTTPServerTransport on a POST /mcp endpoint. OAuth is required, and it actually works now. Three OAuth-flow bugs that broke things in March ( #75 , #85 , #109 on anthropics/claude-ai-mcp) all closed on 2026-03-29. The flow is reliable since. There’s still no field for a static bearer token in the connector UI ( #10 is open), so OAuth is the only path — but you can pre-register one static client via env vars so claude.ai has fixed credentials to paste into Advanced settings. No interactive consent UI to babysit. The MCP server core (~30 lines) import { McpServer } from “@modelcontextprotocol/sdk/server/mcp.js”; import { StreamableHTTPServerTransport } from “@modelcontextprotocol/sdk/server/streamableHttp.js”; import { z } from “zod”; const skills = loadSkillsFromDisk(); // Map<name, {description, content}> function createServer() { const server = new McpServer({ name: “remote-skills”, version: “1.0.0” }); server.tool(“list_skills”, “List available skills”, async () => ({ content: [{ type: “text”, text: JSON.stringify( […skills.values()].map(({name, description}) => ({name, description})) )}], })); server.tool(“load_skill”, “Load skill content”, { name: z.string() }, async ({ name }) => ({ content: [{ type: “text”, text: skills.get(name)?.content ?? “Not found” }], })); return server; } app.post(“/mcp”, oauthMiddleware, async (req, res) => { const server = createServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); The OAuth bit (the part that’s easy to get wrong) Use the SDK’s mcpAuthRouter + a custom OAuthServerProvider. The key trick: pre-register one static client from env vars in the provider’s constructor so it survives container restarts (Render free tier sleeps after 15 min and wipes in-memory state). claude.ai’s redirect URI for the connector callback is fixed: https://claude.ai/api/mcp/auth/_callback . Include it in the pre-registered client’s redirect_uris. class StaticClientOAuthProvider implements OAuthServerProvider { clientsStore = new InMemoryClientsStore(); constructor(clientId: string, clientSecret: string) { this.clientsStore.registerClient({ client_id: clientId, client_secret: clientSecret, redirect_uris: [“https://claude.ai/api/mcp/auth_callback”], grant_types: [“authorization_code”], response_types: [“code”], token_endpoint_auth_method: “client_secret_basic”, }); } // … auto-approve authorize(), exchange code for token, verify tokens } app.use(mcpAuthRouter({ provider: oauthProvider, issuerUrl: new URL(BASE_URL), baseUrl: new URL(BASE_URL), scopesSupported: [“mcp:tools”], resourceServerUrl: new URL(“/mcp”, BASE_URL), // not /sse! })); Generate a random OAUTH_CLIENT_ID + OAUTH_CLIENT_SECRET (e.g. node -e “console.log(require(‘crypto’).randomBytes(32).toString(‘base64url’))”), set them as deploy env vars, plus BASE_URL= https://your-app.your-host.com/ . claude.ai connector setup claude.ai → Settings → Connectors → Add custom connector URL: https://your-app.your-host.com/mcp Click Advanced settings, paste OAuth Client ID + Client Secret from your env vars Save. claude.ai runs the OAuth dance against your server — redirect to /authorize, your provider auto-approves, /token exchange — and the connector appears as Connected. Same connector is automatically available on Claude mobile. Make claude.ai actually use the skills (paste into Settings → Profile → Personal Preferences) I have a custom connector “remote-skills” that serves my personal Claude Code skills - business context, runbooks, design systems, and decision memos. On my first message in any new conversation, if the topic might overlap with something I’d have documented, call list_skills once to see what’s available. If any skill description clearly applies, call load_skill before answering and apply that skill’s guidance. Don’t load speculatively (no match = just answer normally). Don’t recite the skills list out loud - use it silently. This is effectively the system prompt for your Claude — applied to every conversation, web and mobile. Without it, you’d have to remember to say “check my skills” every time. Ready-to-go first prompt (paste into a fresh chat to verify everything works) Use the remote-skills connector to list everything available and give me a one-line summary of what each skill does, grouped by topic. If Claude calls list_skills and returns a grouped summary, you’re done. If it 401s or “can’t reach the connector”, the most common failure is a copy-paste with a trailing newline in the Client ID or Secret — recopy from your generated values. TL;DR Build a small Node MCP server that loads ~/.claude/skills/*/SKILL.md files and exposes list_skills + load_skill over Streamable HTTP at /mcp. Gate with OAuth using a pre-registered static client (env vars survive Render’s free-tier sleep). Generate credentials, paste them into claude.ai’s custom connector Advanced settings. Skills work on web and mobile. The two things to get right: path is /mcp not /sse, and pre-register your OAuth client in env vars (don’t rely on dynamic registration on serverless free tiers — the in-memory store gets wiped on every cold start). submitted by /u/mose_29
Originally posted by u/mose_29 on r/ClaudeCode
What the fuck is this shit. His code probably looks the same.
