I Wired Claude and Cursor Directly Into My Drupal Project Manager

kenneth

Authored on

Image

A few weeks ago I got tired of alt-tabbing. I'd be mid-conversation with Claude about a ticket, then switch to KEBOCA to check the status, copy something, come back. It wasn't painful — but it was friction. And friction adds up.

KEBOCA is my internal Drupal 11 project management tool. Custom built. ECK entities, a Vue-based board, billing. It runs my actual practice — tickets, time tracking, invoicing. Not a SaaS product, not something I'm selling. Just the system I built because the off-the-shelf options didn't fit how I work.

So I decided to wire it up to MCP. Now Claude and Cursor can list tickets, create them, and start timers — in natural language, without touching the Drupal UI. This is how that went.

What MCP Actually Is (The Short Version)

Model Context Protocol is Anthropic's open standard for connecting AI agents to external tools. Think of it as a structured API that LLMs know how to call. You expose tools — functions with defined inputs — and the agent figures out when and how to invoke them based on what you ask.

There's a contrib module for Drupal: drupal/mcp_server. It handles the transport layer and gives you a plugin system (drupal/tool) to define the tools your site exposes. I didn't write the protocol layer — I wrote the tools and connected them to my ECK entities.

The Five Tools

I kept the surface small. Five tools, scoped to what I actually need:

  • list_tickets — lists board tickets with optional filters (status, project, assignee, archived)
  • create_ticket — creates a new ticket with title, description, status, project, priority
  • update_ticket — updates fields on an existing ticket by ID
  • start_timer — starts a time tracker for a ticket
  • stop_timer — stops the active tracker

That covers 90% of what I actually do during a work session. The rest — invoicing, archived project cleanup — doesn't need AI in the loop.

Transport and Auth

The server runs at /_mcp via HTTP Streamable MCP (JSON-RPC 2.0). Locally, it also runs as STDIO through lando drush mcp:server, which is what Cursor uses in dev. Production is straight HTTP with Bearer tokens.

For auth I went with OAuth 2.1 client_credentials via drupal/simple_oauth and drupal/simple_oauth_21. Three scopes: keboca.tickets.read, keboca.tickets.write, keboca.tracker.write. Each tool requires its scope. Anonymous initialize is allowed — everything else needs a token.

Tokens are year-long, issued per operator. No end-user OAuth flows, no public registration. This is an internal tool, so I didn't need anything more complicated.

The Parts That Didn't Just Work

Here's the honest version of the install.

The SDK polyfill

mcp_server expected a RuntimeToolHandlerInterface that mcp/sdk v0.5.0 hadn't declared yet. The interface just didn't exist in that release. I had to write a project-level PHP polyfill and load it through Composer's autoload. Not hard, but the kind of thing you only figure out after staring at the class loader for twenty minutes wondering why Drupal can't find something that's clearly installed.

Circular dependency and addTool() incompatibility

The mcp_server Drush command had a circular service reference on mcp_server.server. On top of that, the tool handler registration was incompatible with mcp/sdk ^0.5's addTool() signature. I fixed both with a custom Composer patch. This is the part of contrib integration nobody writes about — the gap between "this module exists" and "this module works with the current SDK."

The JWT sub claim — the one that broke prod

This was the last blocker, and the most instructive one. client_credentials tokens in OAuth 2.1 don't include a sub claim in the JWT. There's no user subject — the grant is machine-to-machine. Drupal's simple_oauth uses the sub to resolve the consumer's Drupal account, and when it couldn't find one, tool access checks were failing even though the OAuth scope was correct and the Administrator account had all the right ECK permissions.

The smoke test passed locally. Prod failed. That line from my notes: "The smoke test passed. Prod still failed. Turns out, the JWT had no sub." — that's exactly how it went.

The fix was to simplify the Tool API plugin access checks to verify access mcp server permission instead of ECK-level permissions. The OAuth scope — combined with authentication_mode: required on the tool config — is the real security gate. The plugin-level check was redundant and making assumptions about user resolution that don't hold for machine tokens.

The Smoke Test

I wrote a shell script — mcp_http_smoke.sh — to automate the full MCP HTTP flow: initialize → capture session ID → tools/listtools/call with Bearer → assert unauthenticated call fails. This became my definition of done for each environment. Local, dev, prod — all three had to pass the script before I'd call it working.

Having that script saved me probably two hours of manual cURL debugging during the JWT issue. Worth writing it even for a one-person project.

What It Looks Like in Practice

Once it was running, the day-to-day experience is what I was hoping for. Inside a Claude or Cursor conversation:

"List my open tickets for the current project."
"Create a ticket: implement dark mode toggle, assign to me, priority high."
"Start the timer on ticket 42."

No context switching. No copying ticket IDs. The agent has access to the current state of the board and can act on it directly.

25 tickets. Three lines of natural language. No Drupal UI.

What the Contrib Module Gets You

If you're thinking about doing this on your own Drupal site, here's where contrib helps and where it doesn't.

drupal/mcp_server handles the transport layer — HTTP endpoint, STDIO, session management, JSON-RPC routing. That's real work you don't have to do. The Tool API plugin system (drupal/tool) gives you a clean way to define each tool's inputs and outputs as structured schema.

What it doesn't get you: zero-config OAuth, SDK compatibility without patches, or tool access logic that understands machine-to-machine grants out of the box. You're still writing PHP, still patching, still debugging. It's just a much better starting point than rolling the transport from scratch.

Would I Do It Again?

Yes. The JWT issue cost me a couple of hours, the polyfill was annoying, and the Composer patch isn't my favorite artifact to maintain. But the result is genuinely useful in a way I didn't fully anticipate. The friction I was trying to eliminate turned out to be bigger than I realized — I just didn't notice because I'd normalized it.

If you've got a Drupal site with structured content and you're already using Claude or Cursor daily, wiring them together via MCP is worth the afternoon it takes. The contrib module gets you further than I expected, and the gaps are patchable.

Happy coding!