php-mcp-server

projectcues/php-mcp-server

3.2

If you are the rightful owner of php-mcp-server and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.

This repository provides a minimal, shared-hosting-friendly MCP server that exposes tools to LLM clients via JSON-RPC.

Tools
2
Resources
0
Prompts
0

MCP Server (Plug-and-Play)

This repository provides a minimal, shared-hosting-friendly MCP server that exposes tools to LLM clients via JSON-RPC. It is designed to be plug-and-play: drop in tool classes under src/Tools/, and they are auto-discovered and exposed.

Quick Start

  • Requirements: PHP >= 8.0, Composer, Apache or Nginx (Apache .htaccess included for shared hosting).
  • Install dependencies: composer install
  • No server configuration file required. Tools accept credentials per call.
  • Deploy public/ to your web root. Requests route to public/index.php.
  • Call with JSON-RPC over HTTP. Use the sample curl commands below.

Plug-and-Play Design

  • Auto-discovery: All classes under the MCP\Tools namespace in src/Tools/ are discovered and registered.
  • Flexible instantiation: Tools can be constructed using any of these supported patterns:
    • public static function create(MCP\Framework\ToolContext $context): self
    • __construct(MCP\Framework\ToolContext $context)
    • __construct(GuzzleHttp\Client $client, string $apiKey) (backward-compatible)
    • __construct() (no-arg constructor)
  • Context: ToolContext provides configuration and HTTP client factories for services.

Writing a Tool

  1. Create a PHP file in src/Tools/ and define one or more tool classes under the MCP\Tools namespace.

  2. Extend MCP\Framework\Tool and implement:

    • getName(): string
    • getDescription(): string
    • getInputSchema(): array (JSON Schema for tools/call arguments)
    • execute(array $arguments): array
  3. Return results as text objects for MCP:

return ['type' => 'text', 'text' => 'Your result text'];
  1. Choose an instantiation pattern:

Option A: Static factory with ToolContext

use MCP\Framework\ToolContext;
use MCP\Framework\Tool;

class WeatherTool extends Tool {
  private \GuzzleHttp\Client $client;

  public static function create(ToolContext $ctx): self {
    $inst = new self();
    $inst->client = $ctx->makeServiceClient('weather', ['timeout' => 8.0]);
    return $inst;
  }

  public function getName(): string { return 'weather_current'; }
  public function getDescription(): string { return 'Get current weather by city.'; }
  public function getInputSchema(): array {
    return {
      'type': 'object',
      'properties': {
        'city': { 'type': 'string' },
        // If API requires a key, declare and pass per-call:
        // 'api_key': { 'type': 'string' }
      },
      'required': ['city']
    };
  }
  public function execute(array $args): array {
    $resp = $this->client->get('current', [
      'query' => [ 'q' => $args['city'] /*, 'key' => $args['api_key'] */ ]
    ]);
    return ['type' => 'text', 'text' => $resp->getBody()->getContents()];
  }
}

Option B: Constructor with ToolContext

use MCP\Framework\ToolContext;
use MCP\Framework\Tool;

class GithubRepoTool extends Tool {
  public function __construct(private ToolContext $ctx) {}
  public function getName(): string { return 'github_repo_info'; }
  public function getDescription(): string { return 'Fetch GitHub repo details.'; }
  public function getInputSchema(): array { return ['type' => 'object', 'properties' => ['token' => ['type' => 'string'], 'owner' => ['type' => 'string'], 'repo' => ['type' => 'string']], 'required' => ['token','owner','repo']]; }
  public function execute(array $args): array {
    $client = $this->ctx->makeServiceClient('github');
    $resp = $client->get("repos/{$args['owner']}/{$args['repo']}", [ 'headers' => ['Authorization' => 'Bearer ' . $args['token']] ]);
    return ['type' => 'text', 'text' => $resp->getBody()->getContents()];
  }
}

ToolContext Reference

  • get(string $key, $default = null): Read from context settings provided by the server (no config file).
  • getAll(): array: Entire context settings array.
  • makeClient(array $options = []): Client: Create a default HTTP client using provided options.
  • makeServiceClient(string $service, array $options = []): Client: Create client using service-specific options (for example base_uri, headers).

Options merge: Supplied $options override defaults; headers are merged.

Server Endpoints

  • POST /index.php JSON-RPC methods:
    • initialize
    • tools/list
    • tools/call with { name, arguments }
  • GET /index.php?health=1 returns { "status": "ok" }.

CORS

  • Browser clients can call the server directly. The entrypoint sets:
    • Access-Control-Allow-Origin: *
    • Access-Control-Allow-Methods: POST, OPTIONS
    • Access-Control-Allow-Headers: Content-Type
  • Preflight (OPTIONS) requests return 204 with the headers above.

Sample Requests

Initialize

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{}}' \
  https://your-host/index.php

List Tools

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"2","method":"tools/list","params":{}}' \
  https://your-host/index.php

Call Tool (VBOUT examples include per-call api_key)

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"vbout_get_contacts","arguments":{"api_key":"$VBOUT_KEY","limit":10}}}' \
  https://your-host/index.php

GitHub (requires token)

curl -s -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "id":"4",
    "method":"tools/call",
    "params":{
      "name":"github_repo_info",
      "arguments":{ "token":"$GITHUB_TOKEN", "owner":"octocat", "repo":"Hello-World" }
    }
  }' \
  https://your-host/index.php

Configuration

No config.php is required. Tools should set service base URIs explicitly when constructing clients, and credentials must be passed per call (for example api_key).

Shared Hosting Tips

  • .htaccess included: disables directory listing and routes to index.php.
  • Logging: errors are written to storage/error.log.
  • Rate limiting and payload size guard enabled in public/index.php.
  • Avoid storing real secrets in the repo; pass credentials per call.

Development Notes

  • Autoload configured via composer.json (MCP\ -> src/).
  • Tool discovery is cached to reduce filesystem and reflection overhead.
  • Response payloads follow JSON-RPC 2.0 and return result.content as text for LLM-friendly output.
  • Where tool responses are JSON, tools now parse bodies and return structured {"type":"json","data":{...}} when available. If parsing fails, tools fall back to {"type":"text","text":"..."}.
  • VBOUT tools:
    • Use per-call api_key credentials passed in arguments.
    • Hardened HTTP clients with explicit base_uri, timeouts, and JSON headers.
    • Standardized error handling: HTTP 4xx/5xx responses surface messages from JSON when present; otherwise show the raw HTTP status or body.

Connect an LLM

  • Endpoint: POST https://your-host/index.php using JSON-RPC 2.0.
  • Handshake: call initialize, then tools/list to discover available tools.
  • Calls: use tools/call with { name, arguments }. Pass per-call credentials like api_key or token in arguments when required.
  • Results: server returns result.content as an array of objects such as {"type":"text","text":"..."} or {"type":"json","data":{...}}.
  • Errors: standard JSON-RPC codes are used; server may respond with -32000 for tool execution failures and HTTP 4xx for guards (401/405/413/429).

Node.js Example

// Minimal JSON-RPC client for MCP server
const fetch = globalThis.fetch || (await import('node-fetch')).default;

const HOST = 'https://your-host/index.php';

async function rpc(method, params) {
  const body = { jsonrpc: '2.0', id: String(Date.now()), method, params };
  const res = await fetch(HOST, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (json.error) throw new Error(`${json.error.code}: ${json.error.message}`);
  return json.result;
}

// Initialize and list tools
const init = await rpc('initialize', {});
const tools = await rpc('tools/list', {});
console.log('Tools:', tools.tools.map(t => t.name));

// Call VBOUT contacts (per-call api_key)
const contacts = await rpc('tools/call', {
  name: 'vbout_get_contacts',
  arguments: { api_key: process.env.VBOUT_KEY, limit: 10 },
});
console.log('Contacts result:', contacts.content[0]);

// Call GitHub repo info (per-call token)
const gh = await rpc('tools/call', {
  name: 'github_repo_info',
  arguments: { token: process.env.GITHUB_TOKEN, owner: 'octocat', repo: 'Hello-World' },
});
console.log('GitHub result:', gh.content[0]);

Python Example

import os, json, requests, time

HOST = 'https://your-host/index.php'

def rpc(method, params):
    body = { 'jsonrpc': '2.0', 'id': str(int(time.time()*1000)), 'method': method, 'params': params }
    r = requests.post(HOST, headers={
        'Content-Type': 'application/json',
    }, data=json.dumps(body))
    j = r.json()
    if 'error' in j:
        raise RuntimeError(f"{j['error']['code']}: {j['error']['message']}")
    return j['result']

init = rpc('initialize', {})
tools = rpc('tools/list', {})
print('Tools:', [t['name'] for t in tools['tools']])

contacts = rpc('tools/call', {
    'name': 'vbout_get_contacts',
    'arguments': { 'api_key': os.environ['VBOUT_KEY'], 'limit': 10 }
})
print('Contacts result:', contacts['content'][0])

gh = rpc('tools/call', {
    'name': 'github_repo_info',
    'arguments': { 'token': os.environ['GITHUB_TOKEN'], 'owner': 'octocat', 'repo': 'Hello-World' }
})
print('GitHub result:', gh['content'][0])

Best Practices

  • Pass service credentials per call (api_key, token) in arguments instead of storing on the server.
  • Do not store service API keys on the server. VBOUT tools require arguments.api_key and will return Missing api_key if omitted. No config-based fallback is used.
  • Respect rate limits; 60 requests/min default, adjustable in public/index.php.
  • Validate tool inputs according to each tool’s inputSchema from tools/list.# php-mcp-server