Sunday, August 30, 2026
HomePHPPHP MCP Server: Join Your PHP App to AI Brokers

PHP MCP Server: Join Your PHP App to AI Brokers


Your PHP utility already is aware of helpful issues. It might probably discover prospects, verify orders, learn experiences, and name APIs. The issue is that an AI agent can not safely use any of that logic except you give it a transparent door to knock on.

An MCP server offers that door. MCP stands for Mannequin Context Protocol. It offers AI brokers a typical strategy to uncover and name capabilities uncovered by your utility.

On this tutorial, we are going to construct a framework-free PHP MCP server utilizing the official PHP SDK. Our server will let an AI agent discover a buyer, retrieve current orders, and browse a retailer abstract from MySQL.

The agent won’t obtain direct database entry. That might be much less like hiring an assistant and extra like handing a stranger the workplace keys. As a substitute, it may use solely the read-only instruments that our PHP server intentionally exposes.

By the top, you’ll have a working mission that communicates over STDIO and may be examined with MCP Inspector or related to an MCP-compatible AI consumer.

Fast reply: What’s a PHP MCP server?

A PHP MCP server is a PHP program that exposes chosen utility options to AI brokers by the Mannequin Context Protocol.

These options are revealed as MCP capabilities:

  • Instruments carry out actions, similar to discovering a buyer or retrieving current orders.
  • Sources present readable information, similar to a retailer abstract or configuration.
  • Prompts present reusable directions for frequent duties.

For this mission, the request move appears like this:

  1. The AI agent receives a query from the consumer.
  2. The MCP consumer discovers the instruments provided by our PHP server.
  3. The agent selects an acceptable software and provides its arguments.
  4. PHP validates the arguments and runs a ready MySQL question.
  5. The MCP server returns structured information to the agent.
  6. The agent makes use of that information to reply the consumer.

The AI mannequin doesn’t write SQL or join on to MySQL. Your PHP code stays accountable for which operations can be found and what information every operation returns.

What we are going to construct

We are going to construct a small MCP server for an internet retailer. It makes use of plain PHP, the official MCP PHP SDK, and MySQLi. No utility framework is required.

The server exposes these capabilities:

Functionality Title Goal
Software get_customer Finds one buyer by numeric ID
Software get_recent_orders Returns the most recent orders for a buyer
Useful resource retailer://abstract Offers read-only buyer, order, and income totals

The server makes use of STDIO transport. The MCP consumer begins the PHP script as a neighborhood course of and communicates with it by normal enter and output.

This method is an efficient match for native AI purchasers and coding brokers. It additionally retains the primary mission centered. We don’t want an online server, public URL, or authentication layer simply to know the MCP move.

Right here is the mission construction:

php-mcp-server/
├── config/
│   └── database.php
├── sql/
│   └── schema.sql
├── src/
│   ├── Database.php
│   ├── Atmosphere.php
│   ├── StoreCapabilities.php
│   └── StoreRepository.php
├── checks/
│   └── smoke-test.php
├── .env.instance
├── composer.json
├── composer.lock
├── mcp-client-config.instance.json
├── README.md
└── server.php

The database and repository courses deal with information entry. The potential class comprises the operations uncovered by MCP. The server.php file connects these components to the official SDK and begins the server.

Necessities and SDK set up

Earlier than beginning, make sure that your native system has:

  • PHP 8.1 or newer
  • Composer
  • MySQL 8 or MariaDB 10.5 or newer
  • The PHP mysqli, fileinfo, and json extensions

You’ll be able to verify the put in PHP model with this command:

php -v

Create the mission listing and transfer into it:

mkdir php-mcp-server
cd php-mcp-server

Set up the official MCP PHP SDK with Composer:

composer require mcp/sdk:^0.7

The SDK is framework-agnostic. You need to use it in a plain PHP mission or join it to an present utility.

On the time of writing, the official SDK remains to be under model 1.0. Its API might change between minor releases. Maintain the generated composer.lock file in your mission in order that deployments and tutorial checks use the identical dependency variations.

Add PSR-4 autoloading for our utility courses in composer.json:

{
    "title": "phppot/php-mcp-server-demo",
    "description": "A framework-free PHP MCP server backed by MySQL.",
    "sort": "mission",
    "license": "MIT",
    "require": {
        "php": "^8.1",
        "ext-mysqli": "*",
        "mcp/sdk": "^0.7"
    },
    "autoload": {
        "psr-4": {
            "PhppotMcpDemo": "src/"
        }
    },
    "config": {
        "allow-plugins": {
            "php-http/discovery": false,
            "phpdocumentor/shim": true
        },
        "sort-packages": true
    }
}

After saving the file, refresh Composer’s autoloader:

composer dump-autoload

Create the MySQL database

Our MCP instruments want some helpful information to retrieve. Create a file named sql/schema.sql and add the next schema and pattern information.

CREATE DATABASE IF NOT EXISTS php_mcp_demo
    CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

USE php_mcp_demo;

DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS prospects;

CREATE TABLE prospects (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    title VARCHAR(100) NOT NULL,
    e mail VARCHAR(190) NOT NULL,
    metropolis VARCHAR(100) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uq_customers_email (e mail)
) ENGINE=InnoDB;

CREATE TABLE orders (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    customer_id INT UNSIGNED NOT NULL,
    order_number VARCHAR(30) NOT NULL,
    standing ENUM('pending', 'paid', 'shipped', 'cancelled') NOT NULL,
    complete DECIMAL(10, 2) UNSIGNED NOT NULL,
    ordered_at DATETIME NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uq_orders_order_number (order_number),
    KEY idx_orders_customer_date (customer_id, ordered_at),
    CONSTRAINT fk_orders_customer
        FOREIGN KEY (customer_id) REFERENCES prospects (id)
        ON UPDATE CASCADE
        ON DELETE RESTRICT
) ENGINE=InnoDB;

INSERT INTO prospects (title, e mail, metropolis) VALUES
    ('Anita Rao', 'anita@instance.com', 'Chennai'),
    ('Marcus Lee', 'marcus@instance.com', 'Singapore'),
    ('Sofia Martin', 'sofia@instance.com', 'Madrid');

INSERT INTO orders (customer_id, order_number, standing, complete, ordered_at) VALUES
    (1, 'ORD-1001', 'shipped', 79.90, '2026-07-04 10:30:00'),
    (1, 'ORD-1004', 'paid', 149.50, '2026-08-02 14:15:00'),
    (1, 'ORD-1006', 'pending', 32.00, '2026-08-09 09:20:00'),
    (2, 'ORD-1002', 'shipped', 220.00, '2026-07-18 16:45:00'),
    (2, 'ORD-1005', 'cancelled', 45.75, '2026-08-07 11:05:00'),
    (3, 'ORD-1003', 'paid', 99.99, '2026-07-29 08:10:00');

Import the file from the mission listing:

mysql -u root -p < sql/schema.sql

The compound index on customer_id and ordered_at helps the question utilized by our recent-orders software. MySQL can find one buyer’s orders and return the most recent information with out scanning the whole desk.

The demo consists of mounted dates so everybody receives predictable outcomes. In an actual utility, these information would exist already in your database.

Configure the database connection

Database credentials shouldn’t be hard-coded within the server file. Create a file named .env.instance within the mission root:

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=php_mcp_demo
DB_USER=root
DB_PASSWORD=

Copy it to .env and enter the credentials to your native database:

cp .env.instance .env

Don’t commit the actual .env file. Add it to .gitignore:

/vendor/
/.env
/.DS_Store

Load the atmosphere variables

Create src/Atmosphere.php. This small loader reads the native .env file with out including one other package deal.

<?php

declare(strict_types=1);

namespace PhppotMcpDemo;

last class Atmosphere
{
    public static operate load(string $file): void
    {
        if (!is_file($file) || !is_readable($file)) {
            return;
        }

        $strains = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

        if ($strains === false) {
            throw new RuntimeException('Unable to learn the atmosphere file.');
        }

        foreach ($strains as $line) {
            $line = trim($line);

            if (
                $line === ''
                || str_starts_with($line, '#')
                || !str_contains($line, '=')
            ) {
                proceed;
            }

            [$name, $value] = array_map(
                'trim',
                explode('=', $line, 2)
            );

            if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', $title)) {
                proceed;
            }

            if (strlen($worth) >= 2) {
                $first = $worth[0];
                $final = $worth[strlen($value) - 1];

                if (
                    ($first === '"' && $final === '"')
                    || ($first === "'" && $final === "'")
                ) {
                    $worth = substr($worth, 1, -1);
                }
            }

            if (getenv($title) === false) {
                putenv($title . '=' . $worth);
            }
        }
    }
}

Construct the MySQLi connection

Create config/database.php to gather the database settings:

<?php

declare(strict_types=1);

return [
    'host' => getenv('DB_HOST') ?: '127.0.0.1',
    'port' => (int) (getenv('DB_PORT') ?: 3306),
    'name' => getenv('DB_NAME') ?: 'php_mcp_demo',
    'user' => getenv('DB_USER') ?: 'root',
    'password' => getenv('DB_PASSWORD') ?: '',
];

Then create src/Database.php:

<?php

declare(strict_types=1);

namespace PhppotMcpDemo;

use mysqli;
use mysqli_sql_exception;

last class Database
{
    /**
     * @param array{
     *     host: string,
     *     port: int,
     *     title: string,
     *     consumer: string,
     *     password: string
     * } $config
     */
    public static operate join(array $config): mysqli
    {
        mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

        strive {
            $database = new mysqli(
                $config['host'],
                $config['user'],
                $config['password'],
                $config['name'],
                $config['port']
            );

            $database->set_charset('utf8mb4');

            return $database;
        } catch (mysqli_sql_exception $exception) {
            throw new RuntimeException(
                'Database connection failed. Verify the values in .env.',
                earlier: $exception
            );
        }
    }
}

The general public error message doesn’t embrace the database host, username, password, or uncooked MySQL error. The unique exception stays accessible for personal logging, however delicate connection particulars are usually not despatched to the MCP consumer.

Create the repository for secure MySQL queries

The repository is the one class that talks on to MySQL. Preserving SQL out of the MCP software strategies makes the code simpler to learn, check, and reuse.

Create src/StoreRepository.php:

<?php

declare(strict_types=1);

namespace PhppotMcpDemo;

use mysqli;

last class StoreRepository
{
    public operate __construct(
        non-public readonly mysqli $database
    ) {
    }

    /**
     * @return array{
     *     id: int,
     *     title: string,
     *     e mail: string,
     *     metropolis: string
     * }|null
     */
    public operate findCustomer(int $customerId): ?array
    {
        $assertion = $this->database->put together(
            'SELECT id, title, e mail, metropolis
             FROM prospects
             WHERE id = ?'
        );

        $statement->bind_param('i', $customerId);
        $statement->execute();

        $row = $statement->get_result()->fetch_assoc();
        $statement->shut();

        if ($row === null) {
            return null;
        }

        return [
            'id' => (int) $row['id'],
            'title' => (string) $row['name'],
            'e mail' => (string) $row['email'],
            'metropolis' => (string) $row['city'],
        ];
    }

    /**
     * @return record<array{
     *     id: int,
     *     order_number: string,
     *     standing: string,
     *     complete: float,
     *     ordered_at: string
     * }>
     */
    public operate findRecentOrders(
        int $customerId,
        int $restrict
    ): array {
        $assertion = $this->database->put together(
            'SELECT id, order_number, standing, complete, ordered_at
             FROM orders
             WHERE customer_id = ?
             ORDER BY ordered_at DESC, id DESC
             LIMIT ?'
        );

        $statement->bind_param(
            'ii',
            $customerId,
            $restrict
        );

        $statement->execute();
        $outcome = $statement->get_result();
        $orders = [];

        whereas ($row = $result->fetch_assoc()) {
            $orders[] = [
                'id' => (int) $row['id'],
                'order_number' => (string) $row['order_number'],
                'standing' => (string) $row['status'],
                'complete' => (float) $row['total'],
                'ordered_at' => (string) $row['ordered_at'],
            ];
        }

        $statement->shut();

        return $orders;
    }

    /**
     * @return array{
     *     prospects: int,
     *     orders: int,
     *     income: float
     * }
     */
    public operate getStoreSummary(): array
    {
        $outcome = $this->database->question(
            'SELECT
                (SELECT COUNT(*) FROM prospects) AS prospects,
                COUNT(*) AS orders,
                COALESCE(SUM(complete), 0) AS income
             FROM orders'
        );

        $row = $result->fetch_assoc();

        return [
            'customers' => (int) ($row['customers'] ?? 0),
            'orders' => (int) ($row['orders'] ?? 0),
            'income' => (float) ($row['revenue'] ?? 0),
        ];
    }
}

The client ID and outcome restrict are sure as integers. They’re by no means joined instantly into the SQL string. This prevents SQL injection and likewise makes the anticipated enter sorts clear.

The abstract question doesn’t require a ready assertion as a result of it comprises no user-controlled values. Ready statements defend dynamic enter. They aren’t a ceremonial hat that each question should put on.

Outline the MCP instruments and useful resource

The potential class decides what the AI agent is allowed to do. It receives regular PHP values, validates them, calls the repository, and returns structured arrays.

Create src/StoreCapabilities.php:

<?php

declare(strict_types=1);

namespace PhppotMcpDemo;

last class StoreCapabilities
{
    public operate __construct(
        non-public readonly StoreRepository $repository
    ) {
    }

    /**
     * Discover one buyer by their numeric ID.
     *
     * @return array{
     *     discovered: bool,
     *     buyer: array{
     *         id: int,
     *         title: string,
     *         e mail: string,
     *         metropolis: string
     *     }|null
     * }
     */
    public operate getCustomer(int $customerId): array
    {
        if ($customerId < 1) {
            throw new InvalidArgumentException(
                'customerId should be a optimistic integer.'
            );
        }

        $buyer = $this->repository->findCustomer(
            $customerId
        );

        return [
            'found' => $customer !== null,
            'customer' => $customer,
        ];
    }

    /**
     * Checklist a buyer's latest orders.
     * The restrict may be from 1 to twenty.
     *
     * @return array{
     *     customer_id: int,
     *     depend: int,
     *     orders: record<array{
     *         id: int,
     *         order_number: string,
     *         standing: string,
     *         complete: float,
     *         ordered_at: string
     *     }>
     * }
     */
    public operate getRecentOrders(
        int $customerId,
        int $restrict = 5
    ): array {
        if ($customerId < 1) {
            throw new InvalidArgumentException(
                'customerId should be a optimistic integer.'
            );
        }

        if ($restrict < 1 || $restrict > 20) {
            throw new InvalidArgumentException(
                'restrict should be between 1 and 20.'
            );
        }

        $orders = $this->repository->findRecentOrders(
            $customerId,
            $restrict
        );

        return [
            'customer_id' => $customerId,
            'count' => count($orders),
            'orders' => $orders,
        ];
    }

    /**
     * Return a small, read-only abstract of the demo retailer.
     *
     * @return array{
     *     prospects: int,
     *     orders: int,
     *     income: float,
     *     forex: string
     * }
     */
    public operate getStoreSummary(): array
    {
        return [
            ...$this->repository->getStoreSummary(),
            'currency' => 'USD',
        ];
    }
}

The strategy signatures are greater than unusual sort hints. The MCP SDK can examine them and construct the enter schema that an AI consumer makes use of to know every software.

For instance, getRecentOrders() requires customerId and offers restrict a default worth of 5. The SDK turns that data into software metadata as a substitute of constructing us keep a separate JSON schema by hand.

The code nonetheless validates values at runtime. An accurate JSON sort doesn’t assure a wise worth. With out the higher restrict, an agent might request hundreds of orders and create a needlessly massive response.

The strategies return structured arrays as a substitute of fastidiously worded sentences. This offers the AI consumer predictable fields whereas leaving the ultimate clarification to the mannequin.

Construct and run the PHP MCP server

Now we will join the database, repository, capabilities, and MCP SDK.

Create server.php within the mission root:

<?php

declare(strict_types=1);

use McpServer;
use McpServerTransportStdioTransport;
use PhppotMcpDemoDatabase;
use PhppotMcpDemoEnvironment;
use PhppotMcpDemoStoreCapabilities;
use PhppotMcpDemoStoreRepository;

require __DIR__ . '/vendor/autoload.php';

Atmosphere::load(__DIR__ . '/.env');

/**
 * @var array{
 *     host: string,
 *     port: int,
 *     title: string,
 *     consumer: string,
 *     password: string
 * } $databaseConfig
 */
$databaseConfig = require __DIR__
    . '/config/database.php';

strive {
    $database = Database::join($databaseConfig);

    $repository = new StoreRepository($database);
    $capabilities = new StoreCapabilities($repository);

    $server = Server::builder()
        ->setServerInfo(
            title: 'PHPpot Retailer MCP Server',
            model: '1.0.0',
            description: 'Learn-only buyer and order information '
                . 'for the PHP MCP tutorial.'
        )
        ->setInstructions(
            'Use get_customer to confirm a buyer. '
            . 'Use get_recent_orders for order historical past. '
            . 'Learn retailer://abstract solely when a '
            . 'store-wide complete is beneficial.'
        )
        ->addTool(
            handler: [$capabilities, 'getCustomer'],
            title: 'get_customer',
            title: 'Get buyer',
            description: 'Discover one buyer by numeric ID. '
                . 'This software by no means modifications information.'
        )
        ->addTool(
            handler: [$capabilities, 'getRecentOrders'],
            title: 'get_recent_orders',
            title: 'Get current orders',
            description: 'Return the most recent orders for one '
                . 'buyer. The restrict should be from 1 to twenty.'
        )
        ->addResource(
            handler: [$capabilities, 'getStoreSummary'],
            uri: 'retailer://abstract',
            title: 'store_summary',
            title: 'Retailer abstract',
            description: 'Learn-only buyer, order, and '
                . 'income totals for the demo retailer.',
            mimeType: 'utility/json'
        )
        ->construct();

    $server->run(new StdioTransport());
} catch (Throwable $exception) {
    fwrite(
        STDERR,
        '[PHP MCP Server] '
            . $exception->getMessage()
            . PHP_EOL
    );

    exit(1);
}

Server::builder() creates the server and registers every functionality. We use handbook registration as a result of the potential object already comprises a repository created at runtime.

The 2 addTool() calls publish executable operations. The addResource() name publishes a readable URI. Instruments reply a request with arguments, whereas a static useful resource offers information by a identified URI.

The directions assist the AI consumer select the proper functionality. Clear software names and descriptions matter as a result of the mannequin makes use of this metadata when deciding whether or not and the right way to name a software.

StdioTransport reads MCP messages from normal enter and writes responses to plain output. Don’t use echo, print_r(), or var_dump() for debugging on this server. Additional output can corrupt the JSON-RPC dialog. Write diagnostic messages to STDERR or a log file as a substitute.

Take a look at the PHP MCP server with MCP Inspector

The best strategy to check the server earlier than connecting an AI consumer is with MCP Inspector.

Run this command from the mission listing:

npx @modelcontextprotocol/inspector php server.php

The command begins the PHP server and prints a neighborhood Inspector URL. Open that URL in your browser and hook up with the server.

Open the Instruments tab. You must see:

  • get_customer
  • get_recent_orders

Choose get_customer and name it with:

{
    "customerId": 1
}

The outcome ought to include Anita Rao’s buyer file.

Subsequent, name get_recent_orders with:

{
    "customerId": 1,
    "restrict": 3
}

The server ought to return the three latest orders for that buyer. You may as well open the Sources tab and browse retailer://abstract.

MCP Inspector testing the get_recent_orders tool from a PHP MCP server

Testing the PHP MCP server and viewing structured order information in MCP Inspector.

If Inspector lists the instruments and returns the pattern information, the whole MCP path is working. The consumer found the server, learn its software schemas, despatched a software name, and obtained information produced by PHP and MySQL.

Join the PHP server to an AI consumer

An MCP-compatible AI consumer can begin the PHP course of routinely. You solely want to inform the consumer which command to run.

Create mcp-client-config.instance.json with the next configuration:

{
    "mcpServers": {
        "phppot-store": {
            "command": "php",
            "args": [
                "/absolute/path/to/php-mcp-server/server.php"
            ]
        }
    }
}

Exchange the instance path with the actual absolute path to server.php. Then add the phppot-store entry to your consumer’s MCP configuration and restart the consumer.

VS Code AI connecting to PHP App

VS Code AI connecting to PHP App

The precise configuration file location varies between AI purchasers. Some purchasers additionally present a settings display screen the place you may add the command and arguments with out enhancing JSON.

You do not want to run php server.php in a separate terminal. With STDIO transport, the MCP consumer begins and manages the PHP course of.

After the consumer connects, strive questions similar to:

  • Discover buyer 1.
  • Present the three most up-to-date orders for buyer 1.
  • What number of prospects and orders are within the retailer?

The AI agent ought to choose the matching software or useful resource, provide the required values, and use the returned information in its reply.

If the consumer can not discover PHP, exchange "command": "php" with absolutely the path to the PHP executable. Yow will discover it with:

which php

Utilizing absolute paths for each PHP and server.php avoids a typical downside. Desktop AI purchasers might not inherit the identical working listing or command path as your terminal.

Add an automatic MCP smoke check

Inspector is beneficial while you need to discover the server by hand. An automatic smoke check is best while you need to verify that the server nonetheless works after altering the code.

Create checks/smoke-test.php:

<?php

declare(strict_types=1);

use McpClient;
use McpClientTransportStdioTransport;

require dirname(__DIR__) . '/vendor/autoload.php';

$projectDirectory = dirname(__DIR__);

$consumer = Shopper::builder()
    ->setClientInfo(
        'PHP MCP Demo Smoke Take a look at',
        '1.0.0'
    )
    ->setInitTimeout(10)
    ->setRequestTimeout(10)
    ->construct();

strive {
    $client->join(
        new StdioTransport(
            command: PHP_BINARY,
            args: [
                $projectDirectory . '/server.php'
            ],
            cwd: $projectDirectory,
            env: array_merge(
                $_ENV,
                [
                    'DB_HOST' => getenv('DB_HOST')
                        ?: '127.0.0.1',
                    'DB_PORT' => getenv('DB_PORT')
                        ?: '3306',
                    'DB_NAME' => getenv('DB_NAME')
                        ?: 'php_mcp_demo',
                    'DB_USER' => getenv('DB_USER')
                        ?: 'root',
                    'DB_PASSWORD' => getenv('DB_PASSWORD')
                        ?: '',
                ]
            )
        )
    );

    $toolNames = array_map(
        static fn ($software): string => $tool->title,
        $client->listTools()->instruments
    );

    assert(
        in_array(
            'get_customer',
            $toolNames,
            true
        )
    );

    assert(
        in_array(
            'get_recent_orders',
            $toolNames,
            true
        )
    );

    $buyer = $client->callTool(
        'get_customer',
        [
            'customerId' => 1
        ]
    );

    assert($customer->isError === false);

    $orders = $client->callTool(
        'get_recent_orders',
        [
            'customerId' => 1,
            'limit' => 2,
        ]
    );

    assert($orders->isError === false);

    $resourceUris = array_map(
        static fn ($useful resource): string => $resource->uri,
        $client->listResources()->assets
    );

    assert(
        in_array(
            'retailer://abstract',
            $resourceUris,
            true
        )
    );

    $abstract = $client->readResource(
        'retailer://abstract'
    );

    assert(depend($summary->contents) === 1);

    fwrite(
        STDOUT,
        "Smoke check handed.n"
    );
} lastly {
    $client->disconnect();
}

Run the check with assertions enabled:

php -d zend.assertions=1 checks/smoke-test.php

A profitable run prints:

Smoke check handed.

This isn’t a mock check. It begins server.php as a toddler course of, completes the MCP initialization, discovers the capabilities, calls each instruments, reads the useful resource, and checks the responses.

That makes it helpful for catching issues that an unusual PHP unit check might miss, together with damaged server registration, transport errors, and invalid MCP outcome formatting.

Safety issues

An MCP software is an utility endpoint, even when it runs by STDIO. Deal with each software argument as untrusted enter and expose solely the operations an AI agent genuinely wants.

Maintain the software record small and specific

This demo registers three read-only capabilities. The agent can not ship arbitrary SQL, select a desk, or name any PHP methodology it desires.

Don’t create normal instruments similar to run_sql, execute_command, or call_any_api. They might be handy throughout growth, however they take away the security boundary that the MCP server is meant to offer.

Use a restricted database account

The database consumer utilized by the MCP server ought to have solely the permissions required by its instruments. This demo wants SELECT permission solely.

CREATE USER 'mcp_reader'@'localhost'
IDENTIFIED BY 'use-a-strong-password';

GRANT SELECT
ON php_mcp_demo.*
TO 'mcp_reader'@'localhost';

FLUSH PRIVILEGES;

Utilizing a read-only account limits the harm if a future question comprises a mistake or an surprising software name reaches the database.

Validate each argument

Ready statements stop SQL injection, however they don’t resolve whether or not a worth is affordable. The potential class checks that buyer IDs are optimistic and limits every order response to twenty information.

Apply comparable limits to dates, file sizes, search lengths, web page numbers, and API request counts. A sound worth can nonetheless be costly or abusive.

Implement authorization in PHP

Software descriptions are directions for the AI mannequin. They aren’t access-control guidelines.

If one consumer mustn’t see one other consumer’s orders, implement that rule inside PHP earlier than operating the question. Don’t depend on a immediate similar to “solely entry the present consumer’s information.” Prompts can information mannequin habits, however your utility should make the ultimate authorization choice.

Return solely the information the agent wants

The get_customer software returns an e mail handle as a result of it helps display structured information. A manufacturing software ought to omit private data except it’s required for the duty and the caller is allowed to obtain it.

Keep away from returning password hashes, entry tokens, inside notes, uncooked exception traces, or full database rows just because they’re accessible.

Shield HTTP deployments individually

STDIO normally runs regionally below the identical operating-system account because the AI consumer. In case you later expose the server by HTTP, add correct authentication, authorization, TLS, origin validation, request limits, and audit logging.

Altering the transport from STDIO to HTTP doesn’t routinely make the server secure for the general public web.

Maintain STDOUT clear

STDOUT is reserved for MCP messages. Ship utility logs to STDERR or a protected log vacation spot. Apart from breaking the protocol, careless debug output can leak credentials or buyer information into the consumer dialog.

Widespread errors and fixes

Composer can not discover the MCP courses

In case you see a Class "McpServer" not discovered error, set up the dependencies and rebuild the autoloader:

composer set up
composer dump-autoload

Additionally verify that server.php masses vendor/autoload.php.

The database connection fails

Verify that MySQL is operating and that the values in .env are right. You’ll be able to check the credentials instantly:

mysql -h 127.0.0.1 -u root -p php_mcp_demo

In case your MySQL server makes use of a unique port, replace DB_PORT. On some methods, connecting to localhost makes use of a Unix socket whereas 127.0.0.1 makes use of TCP. Switching between them can clarify why the identical credentials work in a single command however fail in one other.

The AI consumer can not begin the server

Use absolute paths within the MCP configuration. Desktop purposes might not use the identical working listing or command path as your terminal.

{
    "mcpServers": {
        "phppot-store": {
            "command": "/absolute/path/to/php",
            "args": [
                "/absolute/path/to/php-mcp-server/server.php"
            ]
        }
    }
}

Restart the AI consumer after altering its MCP configuration.

The instruments don’t seem

Verify the consumer logs or run the server by MCP Inspector. A startup error, lacking dependency, or failed database connection may cause the PHP course of to exit earlier than functionality discovery finishes.

Additionally verify that the software names in server.php are legitimate and that construct() is known as in any case capabilities are registered.

The consumer experiences malformed JSON or a transport error

Search for echo, print_r(), var_dump(), PHP warnings, or debug toolbar output. Any surprising textual content written to STDOUT can combine with MCP’s JSON-RPC messages.

Ship debug messages to STDERR:

fwrite(
    STDERR,
    'Buyer lookup began' . PHP_EOL
);

A software rejects the arguments

Use the precise argument names uncovered by the software schema. The demo expects customerId, not customer_id.

{
    "customerId": 1,
    "restrict": 3
}

The restrict should be between 1 and 20. Invalid values are rejected earlier than the repository runs its question.

The Inspector command is unavailable

MCP Inspector requires Node.js and npm. Affirm that each instructions are put in:

node -v
npm -v

You’ll be able to nonetheless run the included PHP smoke check if you do not need to make use of Inspector.

Developer FAQ

Does an MCP server name an AI mannequin?

Not essentially. This PHP server exposes instruments and assets. The MCP consumer and AI agent resolve when to make use of them. The server itself doesn’t want an AI API key for this instance.

Is MCP the identical as a REST API?

No. A REST API exposes HTTP endpoints for purposes. MCP defines how AI purchasers uncover capabilities, perceive their enter schemas, name instruments, and browse assets.

An MCP software can name an present REST API internally. You do not need to exchange your present APIs so as to add MCP assist.

Can I add MCP to an present PHP utility?

Sure. The official PHP SDK is framework-agnostic. You’ll be able to register present service strategies as instruments or create a skinny functionality class that calls your present utility logic.

Keep away from copying enterprise guidelines into the MCP layer. Reuse the identical providers that your net controllers, scheduled jobs, or API endpoints already use.

What’s the distinction between a software and a useful resource?

A software performs an operation and normally accepts arguments. For instance, get_recent_orders accepts a buyer ID and a outcome restrict.

A useful resource offers readable content material by a URI. Our retailer://abstract useful resource returns a hard and fast sort of store-wide information with out software arguments.

Do I want MySQL to construct a PHP MCP server?

No. An MCP software can name any PHP logic. It might learn a file, contact an API, search a doc retailer, carry out a calculation, or use information already accessible in your utility.

MySQL is used right here as a result of it demonstrates a sensible utility whereas giving PHP clear management over validation and information entry.

Ought to I exploit STDIO or HTTP transport?

Use STDIO when a neighborhood AI consumer begins the MCP server as a toddler course of. It’s easy and works nicely for native instruments and coding brokers.

Use streamable HTTP when purchasers should hook up with a remotely hosted server. HTTP deployments want extra work, together with authentication, authorization, TLS, session dealing with, and request limits.

Can an MCP software replace database information?

Sure, however write instruments want stronger safeguards. Validate each discipline, implement authorization, use transactions, file an audit path, and contemplate requiring consumer affirmation earlier than damaging or expensive actions.

Beginning with read-only instruments is a safer strategy to be taught the protocol and check how an agent selects capabilities.

Is the official PHP MCP SDK steady?

The SDK is official, however releases under model 1.0 are nonetheless thought of experimental. Minor releases might include API modifications. Pin a appropriate model, commit composer.lock, evaluate launch notes, and rerun the smoke check earlier than upgrading.

Conclusion

We constructed a framework-free PHP MCP server that offers AI brokers managed entry to utility information.

The server exposes two instruments and one useful resource. PHP validates each argument, MySQLi ready statements defend the queries, and the agent receives structured outcomes with out getting direct database entry.

An important half is just not the quantity of code. It’s the boundary we created:

  • The AI agent can uncover solely the capabilities we register.
  • PHP decides which inputs are legitimate.
  • The repository controls which queries can run.
  • MySQL returns solely the chosen fields.
  • The MCP consumer receives a predictable outcome.

This sample may be added to an present PHP utility with out changing its present APIs or enterprise logic. Begin with small, read-only instruments. As soon as the permission mannequin is obvious, you may add operations for stock checks, report technology, assist lookups, or different duties that suit your utility.

MCP doesn’t make an AI agent reliable by magic. It offers your PHP utility a structured place to resolve what the agent is allowed to do. That’s the helpful half.

Obtain the PHP MCP server supply code

The downloadable mission comprises the whole framework-free PHP MCP server used on this tutorial.

  • Official mcp/sdk dependency configuration
  • Two read-only MCP instruments
  • One MCP useful resource
  • MySQLi repository with ready statements
  • Pattern MySQL schema and information
  • MCP consumer configuration instance
  • Automated smoke check
  • Native setup and troubleshooting directions

Obtain the PHP MCP server demo ZIP

After extracting the ZIP, run composer set up, import sql/schema.sql, copy .env.instance to .env, and add your database credentials.

You’ll be able to then check the whole mission with:

php -d zend.assertions=1 checks/smoke-test.php

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments