Skip to content

Creating MCP Extensions

Edit this page

MCP extensions are Composer packages that declare themselves using a specific configuration in composer.json, similar to PHPStan extensions.

Quick Start

You can also start from the official extension template: matesofmate/extension-template.

1. Configure composer.json

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "name": "vendor/my-extension",
    "type": "library",
    "require": {
        "symfony/ai-mate": "^0.12"
    },
    "extra": {
        "ai-mate": {
            "scan-dirs": ["src", "lib"],
            "instructions": "INSTRUCTIONS.md"
        }
    }
}

The extra.ai-mate section is required for your package to be discovered as an extension. If your package uses Mate internally but must not be exposed as a reusable extension, set "extension": false in extra.ai-mate.

2. Create MCP Capabilities

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Mcp\Capability\Attribute\McpTool;
use Psr\Log\LoggerInterface;

class MyTool
{
    // Dependencies are automatically injected
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    #[McpTool(name: 'my-tool', description: 'What this tool does')]
    public function execute(string $param): string
    {
        $this->logger->info('Tool executed', ['param' => $param]);

        return 'Result: ' . $param;
    }
}

3. Install and Enable

1
2
$ composer require vendor/my-extension
$ vendor/bin/mate discover

The discover command will automatically add your extension to mate/extensions.php:

1
2
3
return [
    'vendor/my-extension' => ['enabled' => true],
];

When the host project is already initialized, Composer install/update will also refresh discovery automatically through the Mate Composer plugin.

To disable an extension, set enabled to false:

1
2
3
4
return [
    'vendor/my-extension' => ['enabled' => true],
    'vendor/unwanted-extension' => ['enabled' => false],
];

Dependency Injection

Tools, Resources, and Prompts support constructor dependency injection via Symfony's DI Container. Dependencies are automatically resolved and injected.

Configuring Services

Register service configuration files in your composer.json:

1
2
3
4
5
6
7
8
9
10
{
    "extra": {
        "ai-mate": {
            "scan-dirs": ["src"],
            "includes": [
                "config/services.php"
            ]
        }
    }
}

Create service configuration files using Symfony DI format:

1
2
3
4
5
6
7
8
9
10
11
12
// config/services.php
use App\MyApiClient;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return function (ContainerConfigurator $configurator) {
    $services = $configurator->services();

    // Register a service with parameters
    $services->set(MyApiClient::class)
        ->arg('$apiKey', '%env(MY_API_KEY)%')
        ->arg('$baseUrl', 'https://api.example.com');
};

Configuration Reference

Scan Directories

extra.ai-mate.scan-dirs (optional)

  • Default: Package root directory
  • Relative to package root
  • Multiple directories supported

Service Includes

extra.ai-mate.includes (optional)

  • Array of service configuration file paths
  • Standard Symfony DI configuration format (PHP files)
  • Supports environment variables via %env()%

Agent Instructions

extra.ai-mate.instructions (optional)

  • Path to a markdown file containing instructions for AI agents
  • Relative to package root
  • Conventionally named INSTRUCTIONS.md
  • Content is aggregated and provided to AI assistants during MCP handshake

Example configuration:

1
2
3
4
5
6
7
8
{
    "extra": {
        "ai-mate": {
            "scan-dirs": ["src"],
            "instructions": "INSTRUCTIONS.md"
        }
    }
}

Skills

extra.ai-mate.skills (optional)

  • List of directories holding Agent Skills, relative to package root (a single string is also accepted)
  • Each immediate subdirectory is one skill and must contain a SKILL.md file
  • Conventionally a single skills directory
  • When a project runs mate discover (or mate skills:install), each skill directory is installed into the project's .agents/skills/ and symlinked into .claude/skills/ so coding agents can use it

Example configuration:

1
2
3
4
5
6
7
8
{
    "extra": {
        "ai-mate": {
            "scan-dirs": ["src"],
            "skills": ["skills"]
        }
    }
}

The directory layout for the example above:

1
2
3
4
5
skills/
└── my-skill/
    ├── SKILL.md
    └── references/
        └── details.md

Extension Discovery Opt-Out

extra.ai-mate.extension (optional)

  • Default: true
  • Set to false to exclude the package from Mate extension discovery
  • Useful for applications or internal tooling packages that use Mate but should not appear as installable extensions

Example opt-out:

1
2
3
4
5
6
7
8
{
    "extra": {
        "ai-mate": {
            "extension": false,
            "scan-dirs": ["mate/src"]
        }
    }
}

Writing Effective Agent Instructions

Agent instructions help AI assistants understand when and how to use your extension's tools. A good INSTRUCTIONS.md file should:

  1. Map CLI commands to MCP tools - Show what tools replace common CLI operations
  2. Highlight benefits - Explain why the MCP tools are better than alternatives
  3. Be concise - AI assistants have context limits; focus on essential guidance

Example INSTRUCTIONS.md:

1
2
3
4
5
6
7
8
9
10
11
12
13
## My Extension

Use MCP tools instead of CLI for better results:

| Instead of...              | Use                    |
|----------------------------|------------------------|
| `my-cli command`           | `my-tool`              |
| `my-cli search "term"`     | `my-search` with term  |

### Benefits
- Structured output that AI can parse
- Better error handling and context
- Integrated with project configuration

Security

Discovered extensions are managed in mate/extensions.php:

  • The discover command automatically adds discovered extensions
  • All extensions default to enabled: true when discovered
  • Set enabled: false to disable an extension
  • Set extra.ai-mate.extension to false to keep a package out of discovery entirely

Troubleshooting

Extensions Not Discovered

If your extensions aren't being found:

  1. Verify composer.json configuration:

    Ensure your package has the extra.ai-mate section:

    1
    2
    3
    4
    5
    6
    7
    {
        "extra": {
            "ai-mate": {
                "scan-dirs": ["src"]
            }
        }
    }
  2. Run discovery:

    1
    $ vendor/bin/mate discover

    If the host project has already been initialized, Composer install/update should also refresh discovery automatically.

  3. Check the extensions file:

    1
    $ cat mate/extensions.php

    Verify your package is listed and enabled is true.

    If the package intentionally sets extra.ai-mate.extension to false, it will not appear in mate/extensions.php.

Extensions Not Loading

If extensions are discovered but not loading:

  1. Check enabled status in mate/extensions.php:

    1
    2
    3
    return [
        'vendor/my-extension' => ['enabled' => true],  // Must be true
    ];
  2. Verify scan directories exist and contain PHP files with MCP attributes.
  3. Check for PHP errors in your extension code:

    1
    $ php -l src/MyTool.php

Tools Not Appearing

If your MCP tools don't appear in the AI assistant:

  1. Verify MCP attributes are correctly applied:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use Mcp\Capability\Attribute\McpTool;
    
    class MyTool
    {
        #[McpTool(name: 'my-tool', description: 'Description here')]
        public function execute(): string
        {
            return 'result';
        }
    }
  2. Check that classes are in scan directories defined in composer.json.
  3. Restart your AI assistant after making changes.
  4. Check server logs for registration errors.

Tool Execution Fails

If tools are visible but fail when called:

  1. Check return types - tools must return scalar values or arrays:

    1
    2
    3
    4
    5
    6
    // Good
    public function execute(): string { return 'result'; }
    public function execute(): array { return ['key' => 'value']; }
    
    // Bad - objects are not directly serializable
    public function execute(): object { return new stdClass(); }
  2. Check for exceptions in your tool code.
  3. Verify dependencies are properly injected.

Dependency Injection Issues

If dependencies aren't being injected:

  1. Register services in your services.php or config/services.php:

    1
    2
    3
    $services->set(MyService::class)
        ->autowire()
        ->autoconfigure();
  2. Check interface bindings:

    1
    $services->alias(MyInterface::class, MyImplementation::class);
  3. Verify service configuration is listed in composer.json:

    1
    2
    3
    4
    5
    6
    7
    {
        "extra": {
            "ai-mate": {
                "includes": ["config/services.php"]
            }
        }
    }

Agent Instructions Not Loading

If your agent instructions aren't being provided to AI assistants:

  1. Verify the file exists at the path specified in composer.json
  2. Check the path is correct - must be relative to package root:

    1
    2
    3
    4
    5
    6
    7
    {
        "extra": {
            "ai-mate": {
                "instructions": "INSTRUCTIONS.md"
            }
        }
    }
  3. Ensure the file is readable and contains valid markdown
  4. Use debug command to verify discovery:

    1
    $ vendor/bin/mate debug:extensions

    Look for instructions field in the output.

For general server issues and debugging tips, see the Troubleshooting guide.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version