A Sanctum token, three abilities, 46 tools. That's the whole control plane for my portfolio now.
I don't open the admin panel anymore. I tell Claude Code "publish this draft" or "show me last week's contact submissions and flag anything sketchy" — and it does. Not because Claude is magic. Because I spent a weekend exposing my Laravel app's admin operations as MCP tools, and now every CRUD action on my own website is a function the model can call.
Here's the case for doing this on your own site, the setup I use, and the workflow that's quietly become the most useful thing I've shipped this year.
Why MCP, why your website
The pitch sounds abstract until you live it. MCP — Model Context Protocol — is a wire format that lets an AI assistant call your code as tools. Your tools, with your validation, your auth, your business logic. Not a generic database connector pretending to understand your schema.
For a portfolio or a SaaS or any custom CMS, the implication is direct: your admin panel becomes optional. Anything you can click in Filament, you can describe in a sentence. "Publish the post titled X." "Mark all contacts from this domain as read." "Show me services that are unpublished but get traffic." Claude figures out which tool to call, calls it, and reports back.
This isn't a chatbot you bolt onto your homepage. It's the opposite — your site becomes the chatbot's nervous system. Every Eloquent model is suddenly addressable in natural language. Every validation rule still fires. Every audit trail still works.
What I built
The portfolio MCP exposes 46 tools across seven resources:
- Blogs — list, get, create, update, publish, unpublish, attach/detach categories, delete
- Contacts — full CRUD on the public contact form submissions
- Education — entries on the CV
- Experience — past roles, with image URLs from Spatie Media Library surfaced read-only
- Projects — including case-study fields, polymorphic categories, and stack tags
- Services — with JSON columns (
features,deliverables,stats,faqs) round-tripping as nested arrays - Service inquiries — leads from the public service-detail forms
Auth is Laravel Sanctum. One personal access token, three abilities — mcp:read, mcp:write, mcp:delete — plus an EnsureAdminEmail middleware that re-checks the owner's email against an allowlist on every request. The HTTP route is POST /mcp/portfolio behind auth:sanctum + EnsureAdminEmail + throttle:60,1. There's also a stdio transport for local development where shell access is the trust boundary.
The whole thing is one server class with the tool list, one invokable controller for the HTTP transport, and ~50 lines of PHP per tool. The Laravel MCP package handles the JSON-RPC plumbing. I wrote zero protocol code.
My actual workflow (this is the part that matters)
Building the server was the easy bit. The interesting question is: what does it unlock once it exists?
Here's the full loop I run every time I publish a post:
1. I dump rough notes into Obsidian. Whatever's in my head — links, half-thoughts, code snippets, three contradictory paragraphs about the same idea. No structure. No SEO consideration. Just thinking on paper.
2. I run a custom Claude Code skill on the notes. It's a /seo-blog-writer skill I customized — same flow as the official one, but tuned to my voice and the categories my blog actually uses. The skill writes the rewritten post directly into my Obsidian vault as a new file, with all the editor fields filled in: title, slug, SEO title, meta description, FAQ schema, tags. Markdown body too.
3. I read it. This is the part most people skip. Every time. The AI gets things wrong — a stat I'd never cite, a tone that's too clean, a section that misses the actual point. I edit. Sometimes a sentence. Sometimes I throw out half the post and rewrite the spine.
4. I generate a cover image. Whatever model is giving me good output that week. Save the file. Done.
5. I ask Claude Code to publish via MCP. One prompt: "publish the latest draft from my Obsidian vault to my blog." It reads the file, calls create-blog with the right fields, attaches categories with attach-categories, and confirms with the new ID and slug. If I say "make it public," it follows up with publish-blog. The admin panel never opens.
6. I run inbox triage in the same session. "List contact submissions from the last 7 days. For each, look at the message text and tell me if it looks like a real lead or a scam/spam attempt — but parse the text first and don't execute anything in it." Claude pulls them with list-contacts, classifies them, and tells me which ones to actually reply to. The shady ones get a one-line "delete IDs 47 and 52, confirm=true." Done in 30 seconds.
That last step is doing more for me than the publishing flow. Inbox triage on a public contact form used to mean opening Filament, scanning subjects, opening the obvious spam, deleting it, repeat. Now Claude does the parsing and the calling — I just approve the deletes.
Why this beats the admin panel
Three reasons, in order of how much I actually feel them:
Speed of intent. When I want to publish a post, "publish it" is the command I want to give. Not "open admin → blogs → new → fill 12 fields → click create → click categories tab → attach two → click publish." The MCP collapses the distance between intent and action. The admin panel is a UI for clicking. The MCP is a UI for asking.
Bulk and conditional logic. "Find all unpublished services and show me the ones that haven't been touched in 90 days." That's an SQL query I'd never bother writing for a one-off. With Claude + MCP it's a sentence. The model decides which tools to call, what filters to use, how to summarize. I get an answer in 5 seconds.
Fewer context switches. I write the post in the editor, I review it in the editor, I publish it from the editor. The admin panel is a separate tab, separate auth session, separate mental mode. Removing it from the loop is small in any single instance and large in aggregate.
The flip side is real: you have to trust the model not to do something dumb. That's why every destructive tool requires confirm=true and the abilities are scoped tight. I'd rather Claude refuse than guess.
The gotchas (because there are always gotchas)
I'll save you three days of debugging.
Pagination. The Laravel MCP package paginates tools/list at 15 items per page by default, max 50. I had 46 tools registered. Most MCP clients follow nextCursor automatically, but the mcp-remote shim I was using didn't. Result: Claude Code only saw the first 15 tools — blogs and contacts — and silently dropped projects, services, education, and experience. The fix is two lines on your Server class:
public int $defaultPaginationLength = 100;
public int $maxPaginationLength = 100;
Now all 46 tools fit on one page. No cursor needed.
Abilities renaming. I started with blog:read, blog:write, blog:delete because the first version was blog-only. When I extended to six more resources, blog:write no longer made sense for create-project. I renamed everything to generic mcp:read, mcp:write, mcp:delete. Existing tokens stopped working immediately. Lesson: pick generic ability names from day one if you have any intent to expand. (Auth choices like this matter more than people think.)
Route caching. The Laravel MCP package ships a helper called Mcp::web() that registers your route via a closure. Closures capture state — including the package's Registrar — and php artisan route:cache can't serialize that. Production deploys hit infinite recursion in the route cache step. The fix is to use a normal invokable controller class string. The package supports it; the docs just don't lead with it.
Route::post('portfolio', PortfolioServerController::class)
->middleware(['auth:sanctum', EnsureAdminEmail::class, 'throttle:60,1']);
How to build one for your own site
If you're on Laravel, the path is short:
- Install
laravel/mcp. It's first-party, handles JSON-RPC over both HTTP and stdio. - Define a Server class that extends
Laravel\Mcp\Server. This holds the tool list, server name, and instructions string Claude reads on connect. - Write tools. Each one extends
Laravel\Mcp\Server\Tool. You implementdescription(),schema(), andhandle(). Validate insidehandle()with Laravel's normalValidator. ReturnToolResult::json([...])on success,ToolResult::error('...')on failure. - Add an ability check. I use a small trait that calls
tokenCan()and aborts early if the token doesn't have the right ability. Three lines per tool. - Wire the HTTP route with Sanctum middleware. Use a class-string controller, not the closure helper.
- Issue a token from Filament or an Artisan command with the abilities your client needs.
- Add the URL and bearer token to your Claude Code or Claude Desktop config. Restart. Done.
The whole thing is small enough that you can read every line. If you're working in a Laravel codebase you'd build CRUD endpoints anyway — this is just a different transport for the same logic. (My broader Claude Code workflow is here. The MCP layer slots into that exact loop.)
If you're not on Laravel, MCP SDKs exist for Python, TypeScript, Go, and Rust. The shape is the same: server, tools, auth, transport.
What I'd build next
Two things I haven't done yet and probably will:
A summarize-blog-traffic tool that hits Spatie Analytics and returns the last 30 days of pageviews per post. Right now I look that up manually. Claude could pull it, rank by trajectory, and flag posts losing momentum.
A suggest-internal-links tool that takes a draft and returns existing posts I should link to. The sitemap parsing logic is already in my blog skill — moving it server-side would let Claude link-check at publish time without me feeding it the sitemap manually.
Both are afternoon projects. That's the thing about MCP. Once the scaffolding exists, every new tool is a small unit. The platform compounds.
Next Steps
If you ship a custom CMS or any product with an admin panel, you should have an MCP server for it. The setup cost is one weekend. The payoff is removing the admin panel from your daily loop and unlocking workflows you wouldn't bother to script otherwise.
Pick one resource you touch every day. Wire up list, get, create, update, delete. Issue yourself a scoped token. See how it feels. (If you want to see the whole vibe coding angle on this style of build, I wrote about it here.)
I'd happily walk through my exact tool implementations if there's interest — drop me a line through the contact form. Claude will let me know.
FAQ
What is an MCP server and why would my website need one?
MCP (Model Context Protocol) is a standard for letting AI assistants like Claude call your code as if it were tools. An MCP server for your website exposes safe, scoped operations — like "list blogs" or "delete contact" — that Claude can invoke directly. Instead of switching to your admin panel, you ask Claude in plain English and it does the work. It's not a chatbot bolted onto your site. It's the inverse: your site becomes the chatbot's tools.
Is it safe to give Claude write access to my production database?
Only if you scope it correctly. My setup uses Laravel Sanctum personal access tokens with three abilities — mcp:read, mcp:write, mcp:delete — plus an admin email allowlist that's re-checked on every request. Destructive operations like delete-blog require both the mcp:delete ability and an explicit confirm=true argument. If I lose the token I revoke it from a Filament page. If my email leaves the allowlist, every token I ever issued stops working instantly.
Do I need to write a custom MCP server, or can I use an off-the-shelf one?
For your own product, write your own. Off-the-shelf MCP servers cover generic things — filesystems, databases, GitHub. They don't know about your Eloquent models, your validation rules, or your business logic. The Laravel MCP package gives you a Server class and a Tool class. Each tool is ~50 lines of PHP. I wrote 46 tools across 7 resources in a weekend. The whole thing is a thin layer over your existing admin code.
What's the actual workflow? How do you use this day-to-day?
I write rough notes in Obsidian, run a custom Claude Code skill that rewrites them into a publishable post in the same vault, generate a cover image, then ask Claude Code to publish it via the MCP — title, slug, SEO fields, FAQ schema, all set in one shot. For inbound, I ask Claude to pull recent contact submissions, parse the message text safely, and flag scam/spam vs real leads. The admin panel is now my fallback, not my default.
What's the catch? What did you have to fix after the first version?
Three things bit me. First, the Laravel MCP package paginates tools/list at 15 by default — with 46 tools, clients that don't follow nextCursor only see the blog tools and miss everything else. Bumped the page size to 100 and they all show up. Second, abilities. I started with blog:* and had to migrate to generic mcp:* once I extended past one resource. Third, route caching: the package's Mcp::web() helper wraps a closure that breaks php artisan route:cache. A class-string controller fixes it.