A Model Context Protocol (MCP) Server for My Website

Large Language Models (LLMs) are ubiquitous these days. The pace of advancements is staggering, and it's challenging to keep up with the constant updates—it's absolutely insane!

Amidst the overwhelming information, I found the concept of an agent, especially with the Model Context Protocol (MCP), particularly intriguing and promising. So, I decided to experiment with it and see how it could enhance my workflow.

Note

Prefer watching a video? Check out the video version of this article on YouTube.

The Challenge with Identifiers

Currently, my website consists of two key components:

  • The frontend, built with VitePress, where each page is a Markdown file.
  • The backend, a Laravel application that adds additional features and functionality.

To ensure the correct content is served and stored for each frontend page, I need to share an identifier between these components. Each Markdown file has a unique identifier in its frontmatter.

md
---
id: 1
title:
description:
---

This identifier fetches the corresponding content from the backend when loading the page. For everything to work seamlessly, the backend must also store this identifier in the database. Without it, maintaining a functional relational database is impossible, as my database includes a posts table.

To obtain these identifiers, the backend routinely fetches a JSON file generated at build time by VitePress. This file contains all the Markdown files and their frontmatter. This approach works well, but there's a catch.

When writing a new Markdown file, I must access the backend administration page, sort the posts table by the id column, and find the last used identifier. Then, I write this identifier, incremented by one, into the file I'm working on. It's a tedious task that adds no real value and consumes time—a process I thoroughly dislike.

Simultaneously, I frequently leverage LLMs to enhance my articles' technical content, including SEO, grammar, and spelling. This led me to wonder: Can I integrate an agent within the current LLM process to ensure that the identifier is always correct?

Short answer: yes!

Implementing a New Backend Route

MCP servers aren't mystical; they provide a means for LLMs to interact with data sources. Hence, I need to create a backend API route to return the next post identifier. This route will be accessed by the MCP server via the tools capability.

Creating this API route in my Laravel application and setting up an invokable controller is fairly straightforward:

php
use App\Http\Controllers\NextPostIdController;
use Illuminate\Support\Facades\Route;

Route::get('posts/next-id', NextPostIdController::class)
    ->name('posts.next-id');
php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\JsonResponse;

class NextPostIdController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(): JsonResponse
    {
        $nextPostId = Post::max('id') + 1;

        return response()->json(['next_post_id' => $nextPostId]);
    }
}

This setup, although simple, is crucial for an effective MCP server.

Configuring the MCP Server

For the MCP server, I'm utilizing the official TypeScript SDK. It's straightforward and highly efficient.

ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
  CallToolRequestSchema,
  ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js'
import { ofetch } from 'ofetch'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'

const server = new Server(
  {
    name: 'mcp.soubiran.dev',
    version: '0.0.0'
  },
  {
    capabilities: {
      tools: {}
    }
  }
)

const GetNextId = z.object({})

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'next_post_id',
        description: 'Get the next post ID for a post on soubiran.dev',
        inputSchema: zodToJsonSchema(GetNextId),
      },
    ]
  }
})

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'next_post_id') {
    try {
      const data = await ofetch<{ next_post_id: number }>('http://localhost:8000/api/posts/next-id')

      return {
        content: [{
          type: 'text',
          text: data.next_post_id
        }]
      }
    }
    catch (error) {
      throw new Error('Error fetching next post ID')
    }
  }

  throw new Error('Unknown tool')
})

const transport = new StdioServerTransport()
await server.connect(transport)

This code establishes a STDIO MCP server with two key request handlers:

  1. The ListToolsRequest handler returns the available tools to the agent. Currently, there is one tool: next_post_id.
  2. The CallToolRequest handler is activated when the agent uses the tool. It calls the backend API route to retrieve the next post ID.

The tool call process is simple; it fetches the next post ID from the backend API route I've defined, using the ofetch library for ease. The post ID is then returned as a number.

Consequently, any agent invoking this tool will receive the next post ID as a response within its context. This allows me to draft Markdown files without worrying about post IDs—the agent handles it perfectly!

Note

For the complete code, watch the YouTube video.

Does It Work?

Yes, it works seamlessly! See it in action:

Requesting the agent to enhance a file and insert the correct ID automatically using the next post ID from the MCP server.

With this MCP server operational, I can now interact with my Markdown files while supplying context to the agent. This allows me to focus on content rather than technical details. My ultimate aim is to hasten my writing process, making it more efficient and enjoyable, and this software is a step towards that goal.

I'm thoroughly impressed by the MCP SDK's user-friendliness and integration with VS Code. The future of agents is promising, and I highly recommend experimenting with them!

PP

Thanks for reading! My name is Estéban, and I love to write about web development.

I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.

If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.

I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!

Reactions

Discussions

Add a Comment

You need to be logged in to access this feature.

Login with GitHub
Support my work
Follow me on