LLM工具服务器

一个即插即用的MCP服务器,通过JSON-RPC向LLM客户端暴露工具,适用于共享托管环境。

  • by projectcues
  • PHP
  • 本地部署
  • 更新于
在 GitHub 查看 projectcues/php-mcp-server

使用说明

MCP 服务器(即插即用)

这个仓库提供了一个最小化、适合共享主机的MCP服务器,该服务器通过JSON-RPC向大型语言模型(LLM)客户端暴露工具。它设计为即插即用:只需将工具类放入指定位置即可 src/Tools/并且它们会自动被发现并暴露出来。

快速入门

  • 要求: PHP >= 8.0, ComposerApache 或 Nginx(Apache .htaccess (包含在共享托管中)。
  • 安装依赖项: composer install
  • 无需服务器配置文件。工具每次调用时接受凭据。
  • 部署 public/ 到您的网站根目录。请求将被路由到 public/index.php。
  • 通过HTTP使用JSON-RPC进行调用。请参考示例 curl 以下命令。

即插即用设计

  • 自动发现:以下包中的所有类 MCP\Tools 命名空间 in src/Tools/ 被发现并登记。
  • 灵活实例化:可以使用以下任意一种支持的模式来构建工具:
    • public static function create(MCP\Framework\ToolContext $context): self
    • __construct(MCP\Framework\ToolContext $context)
    • __construct(GuzzleHttp\Client $client, string $apiKey) (向后兼容的)
    • __construct() (无参构造函数)
  • 背景: ToolContext 为服务提供配置和HTTP客户端工厂。

编写一个工具

  1. 在(某个位置)创建一个PHP文件 src/Tools/ 并在其下定义一个或多个工具类 MCP\Tools 命名空间。

  2. 扩展 MCP\Framework\Tool 并实施:

    • getName(): string
    • getDescription(): string
    • getInputSchema(): array (用于JSON Schema的 tools/call (参数)
    • execute(array $arguments): array
  3. 将结果作为文本对象返回给MCP:

return ['type' => 'text', 'text' => 'Your result text'];
  1. 选择一个实例化模式:

选项A:带有(某功能/特性)的静态工厂 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()];
  }
}

选项B:带(参数)的构造函数 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()];
  }
}

工具上下文引用

  • get(string $key, $default = null)从服务器提供的上下文设置中读取(无配置文件)。
  • getAll(): array整个上下文设置数组。
  • makeClient(array $options = []): Client使用提供的选项创建一个默认的HTTP客户端。
  • makeServiceClient(string $service, array $options = []): Client使用服务特定选项创建客户端(例如 base_uri, headers)。

选项合并:已提供 $options 覆盖默认值;头部信息将被合并。

服务器终端点(或服务器端点)

  • POST /index.php JSON-RPC 方法:
    • initialize
    • tools/list
    • tools/call 与;带有;使用 { name, arguments }
  • GET /index.php?health=1 回报;收益 { "status": "ok" }。

CORS(跨源资源共享)

  • 浏览器客户端可以直接调用服务器。入口点设置为:
    • Access-Control-Allow-Origin: *
    • Access-Control-Allow-Methods: POST, OPTIONS
    • Access-Control-Allow-Headers: Content-Type
  • 预飞行检查(Preflight)OPTIONS) 请求返回 204 附上上述标题。

样品请求

初始化

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

列出工具

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

调用工具(VBOUT 示例包括每调用一次 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(需要令牌)

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

配置

不 config.php 需要满足此要求。在构建客户端时,工具应明确设置服务基础URI,并且每次调用都必须传递凭据(例如 api_key)。

共享主机使用技巧

  • .htaccess 包含:禁用目录列表并路由到 index.php。
  • 日志记录:错误信息被写入 storage/error.log。
  • 已启用速率限制和有效载荷大小保护 public/index.php。
  • 避免在代码仓库中存储真实密钥;每次调用时传递凭据。

开发说明

  • 通过配置自动加载 composer.json (MCP\ -> src/)。
  • 工具发现结果会被缓存,以减少文件系统和反射操作的开销。
  • 响应有效载荷遵循JSON-RPC 2.0标准并返回 result.content 作为适合大型语言模型(LLM)输出的文本。
  • 当工具响应为JSON格式时,工具现在会解析请求体并返回结构化数据 {"type":"json","data":{...}} 在可用时。如果解析失败,工具将回退到 {"type":"text","text":"..."}。
  • VBOUT 工具:
    • 按每次通话使用 api_key 传入的凭据 arguments.
    • 具有明确配置的加固HTTP客户端 base_uri,超时设置,以及JSON头部信息。
    • 标准化错误处理:当存在时,HTTP 4xx/5xx 响应会从 JSON 中显示消息;否则,显示原始的 HTTP 状态码或响应体。

连接一个大型语言模型(LLM)

  • 终端点: POST https://your-host/index.php 使用 JSON-RPC 2.0。
  • 握手:通话 initialize,然后 tools/list 发现可用的工具。
  • 呼叫:使用 tools/call 与……一起;带着;具有 { name, arguments }传递每次调用的凭据,如 api_key 或者 token 在 arguments 在需要时。
  • 结果:服务器返回 result.content 作为一系列对象,如 {"type":"text","text":"..."} 或者 {"type":"json","data":{...}}。
  • 错误:使用标准的JSON-RPC代码;服务器可能会返回 -32000 对于工具执行失败,以及用于防护的HTTP 4xx状态码(401/405/413/429)。

Node.js 示例

// 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 示例

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])

最佳实践

  • 每次调用时传递服务凭据api_key, token) 在 arguments 而不是存储在服务器上。
  • 不要在服务器上存储服务API密钥。VBOUT工具需要 arguments.api_key 并且将会返回 Missing api_key 如果省略,则不使用基于配置的回退。
  • 遵守速率限制;默认60次请求/分钟,可调整 public/index.php。
  • 根据每种工具的要求验证工具输入 inputSchema 来自 tools/list.# php-mcp-server(可译为:“# PHP-MCP 服务器”或根据上下文具体含义调整,但直译即为上述)