MCP — Model Context Protocol

Textstem includes a built-in Model Context Protocol (MCP) server that exposes your site's content and actions to AI assistants such as Claude Desktop, Cursor, Windsurf, and any other MCP-compatible client.

When enabled, the package registers two HTTP endpoints that speak the MCP Streamable HTTP transport (spec 2024-11-05):

Method Path Purpose
POST /mcp JSON-RPC 2.0 — all MCP requests and responses
GET /mcp SSE endpoint — sends a single endpoint event for clients that open a persistent connection

The path is configurable via MCP_PATH (default: mcp).


Configuration

Add the following to your .env file to enable MCP:

MCP_ENABLED=true

All available options:

MCP_ENABLED=false           # Must be true to register the /mcp routes
MCP_PATH=mcp                # URL path for the endpoints (default: mcp)
MCP_SERVER_NAME=            # Defaults to APP_NAME
MCP_SERVER_VERSION=1.0.0    # Reported in the initialize handshake
MCP_RATE_LIMIT=60           # Max requests per minute per IP
MCP_INSTRUCTIONS=           # Optional instructions included in the initialize response
MCP_SITE_DESCRIPTION=       # Plain-text site description exposed via textstem://site-info

The corresponding config block in config/textstemapp.php (after publishing):

'mcp' => [
    'enabled'          => env('MCP_ENABLED', false),
    'path'             => env('MCP_PATH', 'mcp'),
    'server_name'      => env('MCP_SERVER_NAME', env('APP_NAME', 'Textstem')),
    'server_version'   => env('MCP_SERVER_VERSION', '1.0.0'),
    'rate_limit'       => env('MCP_RATE_LIMIT', 60),
    'instructions'     => env('MCP_INSTRUCTIONS', ''),
    'site_description' => env('MCP_SITE_DESCRIPTION', ''),
    'contact_info'     => [],  // set in AppServiceProvider
],

Built-in Resources

The package automatically registers three read-only resources. Resources are accessed via resources/read with a URI string.

textstem://site-info

Returns general site metadata drawn from config('app.name'), config('app.url'), and the mcp.site_description / mcp.contact_info config values.

{
  "name": "My Site",
  "url": "https://example.com",
  "description": "A brief description of the site.",
  "contact": {
    "email": "hello@example.com"
  }
}

textstem://pages and textstem://pages/{slug}

  • textstem://pages — returns a JSON array of all published, public WranglerPage records (id, title, URL, description, whether listed in menus).
  • textstem://pages/{slug} — returns the full detail of a single page: title, URL, description, keywords, template.

textstem://posts/{type} and textstem://posts/{type}/{slug}

  • textstem://posts/blog — returns a paginated list of published blog posts.
  • textstem://posts/project — returns published projects.
  • textstem://posts/news — returns published news items.
  • textstem://posts/{type}/{slug} — returns full post detail: title, description, keywords, content, published date, tags, canonical URL.

The exposed post types are derived automatically from Collection::allTypes(), so any custom post type registered in your CMS appears here without any additional configuration.


Built-in Tools

The package registers one tool automatically.

search_content

Searches published posts across title, description, and keywords fields.

Input schema:

Parameter Type Required Description
query string yes Keyword or phrase to search for
type string no Filter by post type (e.g. blog, project)
limit integer no Max results (default 10, max 50)

Example response:

{
  "query": "web design",
  "count": 3,
  "results": [
    {
      "uri": "textstem://posts/project/redesign-2024",
      "title": "Website Redesign 2024",
      "type": "project",
      "description": "A full redesign of the company website.",
      "published_at": "2024-03-01T00:00:00+00:00"
    }
  ]
}

Registering App-Specific Tools

App-level tools (e.g. contact forms, newsletter subscriptions) depend on your application's own models and actions, so they are registered in your AppServiceProvider rather than in the package.

Step 1 — Create the tool class using the generator:

php artisan textstem:make:mcp-tool ContactTool

This creates app/Mcp/Tools/ContactTool.php with the stub filled in. Implement the three abstract methods:

namespace App\Mcp\Tools;

use App\Actions\ContactFormSubmitted;
use App\Models\Contact;
use Medialight\Textstem\Mcp\McpTool;

class ContactTool extends McpTool
{
    public function name(): string
    {
        return 'submit_contact';
    }

    public function description(): string
    {
        return 'Submit a contact enquiry to the site team.';
    }

    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'name'    => ['type' => 'string', 'description' => 'Full name.'],
                'email'   => ['type' => 'string', 'format' => 'email'],
                'message' => ['type' => 'string'],
            ],
            'required' => ['name', 'email', 'message'],
        ];
    }

    public function call(array $arguments): array
    {
        $contact = Contact::create([
            'name'  => $arguments['name'],
            'email' => $arguments['email'],
            'body'  => $arguments['message'],
        ]);

        (new ContactFormSubmitted)->contacted($contact);

        return $this->success('Your enquiry has been submitted.');
    }
}

The McpTool base class provides two helpers:

$this->success(string $text): array  // isError: false
$this->error(string $text): array    // isError: true

Step 2 — Register the tool in AppServiceProvider::boot():

use App\Mcp\Tools\ContactTool;
use Medialight\Textstem\Mcp\McpRegistry;

public function boot(): void
{
    if (config('textstemapp.mcp.enabled', false)) {
        $this->app->make(McpRegistry::class)
            ->registerTool(new ContactTool);
    }
}

Registering Custom Resources

To expose additional read-only data (e.g. events, team members, custom post types with special formatting):

Step 1 — Generate the resource class:

php artisan textstem:make:mcp-resource EventsResource

This creates app/Mcp/Resources/EventsResource.php. Implement the three abstract methods:

namespace App\Mcp\Resources;

use App\Models\Event;
use Medialight\Textstem\Mcp\McpResource;

class EventsResource extends McpResource
{
    private const BASE_URI = 'textstem://events';

    public function entries(): array
    {
        return [
            [
                'uri'         => self::BASE_URI,
                'name'        => 'Upcoming Events',
                'description' => 'All upcoming public events.',
                'mimeType'    => $this->mimeType(),
            ],
        ];
    }

    public function handles(string $uri): bool
    {
        return $uri === self::BASE_URI || str_starts_with($uri, self::BASE_URI . '/');
    }

    public function read(string $uri): string
    {
        $events = Event::upcoming()->public()->get(['title', 'slug', 'starts_at']);

        return json_encode(['events' => $events], JSON_PRETTY_PRINT);
    }
}

Step 2 — Register it in AppServiceProvider::boot():

use App\Mcp\Resources\EventsResource;
use Medialight\Textstem\Mcp\McpRegistry;

public function boot(): void
{
    if (config('textstemapp.mcp.enabled', false)) {
        $this->app->make(McpRegistry::class)
            ->registerResource(new EventsResource);
    }
}

Multiple resources and tools can be chained:

$this->app->make(McpRegistry::class)
    ->registerResource(new EventsResource)
    ->registerResource(new TeamResource)
    ->registerTool(new ContactTool)
    ->registerTool(new SubscribeTool);

Connecting to Claude Desktop

Add the following to your Claude Desktop claude_desktop_config.json:

{
  "mcpServers": {
    "my-site": {
      "url": "https://your-site.com/mcp"
    }
  }
}

For local development under Laravel Herd:

{
  "mcpServers": {
    "my-site-local": {
      "url": "https://my-site.test/mcp"
    }
  }
}

After saving, restart Claude Desktop. The site's resources and tools will appear in the context panel.


Security Considerations

  • All endpoints are public — no authentication is required for MCP requests by default. Do not expose sensitive data (admin records, private user data) through MCP resources or tools.
  • Rate limiting is applied to all requests via MCP_RATE_LIMIT (default 60 req/min per IP). Reduce this if you experience abuse.
  • The MCP_ENABLED=false default means MCP is opt-in. No routes are registered unless explicitly enabled.
  • Tool calls that write data (e.g. creating contacts or subscribers) should include their own input validation and should not bypass existing business logic or access controls.

Protocol Reference

The implementation follows the MCP Specification 2024-11-05. Supported JSON-RPC methods:

Method Description
initialize Handshake — returns server info and capabilities
ping Health check
resources/list List all available resources
resources/read Read a resource by URI
resources/templates/list Returns empty (URI templates not used)
tools/list List all available tools
tools/call Invoke a tool by name
prompts/list Returns empty (prompts not implemented)
notifications/* Accepted silently (no response)

Batch requests (array of JSON-RPC objects in a single POST body) are supported.

esc