Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleMCP

A minimal, zero-dependency Model Context Protocol (MCP) server written in PHP ≥ 8.4. It speaks JSON-RPC 2.0 and exposes tools to MCP clients such as Claude Desktop, Cherry Studio, or Open WebUI. In HTTP mode it also ships a self-hosted OAuth 2.1 Authorization Server (Authorization Code + PKCE, dynamic client registration, and discovery metadata) so clients can log in with real user accounts.

printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n' | php index.php

Features

  • MCP protocol — initialize, tools/list, tools/call, notifications/initialized (protocol version 2024-11-05).
  • Three transports from a single entry point:
    • stdio — line-delimited JSON-RPC over stdin/stdout, how MCP clients launch the server.
    • Streamable HTTP — direct JSON-RPC POST (/ or /mcp) with optional Mcp-Session-Id handling.
    • SSE MCP (Server-Sent Events) — standard MCP SSE stream (GET /sse) with message receiver (POST /message?sessionId=...).
  • File-per-Page (PRG pattern) — each web page is a dedicated file that strictly follows the Post/Redirect/Get pattern with flash messages to prevent form re-submissions.
  • Standalone File-per-API — every API endpoint (OAuth token, client registration, passkey options/verify, discovery metadata, MCP JSON-RPC, and SSE) lives in its own standalone script.
  • Attribute-based tools — drop a class in src/Tools/ with #[McpFunction] methods and it is auto-discovered; no manual registration.
  • Per-tool access control — tools declare required roles / permissions; unauthorized tools are hidden from tools/list and rejected at tools/call with JSON-RPC -32001.
  • User context — the current UserContext is injected into any tool method that asks for it. In stdio mode every request runs as a trusted local user with full access.
  • Self-hosted OAuth 2.1 — interactive login page, token endpoint, RFC 7591 dynamic client registration, RFC 8414 / RFC 9728 discovery. Tokens are sha256-hashed at rest.
  • WebAuthn Passkeys — passwordless biometric and hardware security key logins (FIDO2 / WebAuthn).
  • Email Two-Factor Authentication (2FA) — 6-digit OTP verification for new devices, with trusted device cookies (90-day persistence) and zero external dependencies. Copy config/mail.example.php to config/mail.php to configure SMTP or log driver.
  • User management page (/account) — login, public onboarding, change password, passkey registration, email management, trusted devices, and logout.
  • SQLite storage — users, OAuth clients, passkeys, trusted devices, and tokens in data/app.sqlite (gitignored), seeded from config/users.php on first run.
  • No dependencies — composer.json declares only php >= 8.4.

Requirements

  • PHP ≥ 8.4 (uses json_validate(), match, readonly classes, and union types).

Quick start

stdio mode (MCP clients)

MCP clients launch the dedicated stdio script directly:

{
  "mcpServers": {
    "simplemcp": {
      "command": "php",
      "args": ["D:\\Project\\SimpleMCP\\stdio.php"]
    }
  }
}

Smoke test:

printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n' | php stdio.php

HTTP mode (Isolated Document Root & Public Deployment)

For local development:

php -S localhost:8000 -t public/

For production/public web servers (Nginx/Apache/Caddy):

  • Point your web server document root to the public/ directory.
  • This isolates data/ (SQLite databases, logs, sessions) and config/ completely outside the web root.
  • All HTTP responses carry security headers (X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin).
  • Cookies are hardened (HttpOnly, SameSite=Lax, and Secure on HTTPS).
  • Configure a canonical issuer in config/oauth.php to prevent Host-header poisoning.

Included tools

All tools on this branch are examples that demonstrate the server's capabilities:

Tool Description Access
add_numbers Addition of two numbers public
subtract_numbers Subtraction of two numbers public
multiply_numbers Multiplication of two numbers public
divide_numbers Division of two numbers (rounded to 2 dp) public
power_numbers Base raised to an exponent public
calculate_loan_installment Fixed monthly loan payment (PMT formula), with total paid and interest public
get_system_time Current server time (ISO 8601) public
get_current_user The authenticated caller (username, name, roles, permissions) public
server_status Server runtime info admin (roles: ['admin'])

Adding a tool

Tools live in src/Tools/. Create a class whose methods are decorated with #[McpFunction(name, description, schema)]. Each method receives the raw arguments array and returns an array of MCP content blocks.

<?php

declare(strict_types=1);

namespace McpServer\Tools;

use McpServer\Attributes\McpFunction;

readonly class GreetingTool {
    #[McpFunction(
        name: 'greet',
        description: 'Greets a person by name.',
        schema: [
            'type' => 'object',
            'properties' => [
                'name' => ['type' => 'string', 'description' => 'The name to greet'],
            ],
            'required' => ['name'],
        ],
    )]
    public function greet(array $arguments): array {
        $name = $arguments['name'] ?? 'world';
        return [['type' => 'text', 'text' => "Hello, $name!"]];
    }
}

src/McpServer.php calls registerToolsFromDirectory(), which auto-discovers every non-abstract class in src/Tools/ containing at least one #[McpFunction] method — no manual registration step.

Restricting a tool

Pass roles and/or permissions to the attribute (see AdminTool.php):

#[McpFunction(
    name: 'server_status',
    description: 'Returns server runtime information. Admin-only.',
    schema: ['type' => 'object', 'properties' => new \stdClass()],
    roles: ['admin'],
)]

Access rule: a tool that declares a category requires the caller to match within every declared category (any match within a category suffices). A tool with no roles / permissions is public — anonymous HTTP callers can list and call it.

Reading the current user

Declare an optional ?UserContext $user = null parameter (explicit ? — implicit-nullable is deprecated in PHP 8.4). The server detects it via reflection at registration and injects the context at call time; the arguments array stays at parameter index 0 (UserInfoTool.php):

public function getCurrentUser(array $arguments, ?UserContext $user = null): array {
    $user ??= UserContext::anonymous();
    return [['type' => 'text', 'text' => json_encode($user->toArray())]];
}

Users & access control

Users are seeded from config/users.php into SQLite only when the users table is empty. On this branch the seed is:

Username Password Roles Status
admin admin123 admin active
alice secret user active
bob (none yet) user pending

These are dev-only seed credentials — change them before deploying anywhere real.

A user added with no password (status pending) must complete onboarding: the first time they log in — via /account or the OAuth flow a tool call triggers — they are asked to set a password. Their provisioned username is kept and is not editable. /account/onboard is the public path for brand-new users, who choose their own username. New accounts are granted no special roles ([]); grant admin (or other roles) by editing config/users.php or the users table.

In stdio mode there is no HTTP layer: every request runs as the trusted local user with the * wildcard role/permission, so all tools are visible and callable.

HTTP authentication (OAuth 2.1)

When served over HTTP, requests are optionally authenticated:

  • No bearer token → the caller is UserContext::anonymous(); only tools with no roles/permissions are listed and callable.
  • Present but invalid/expired token → 401 with a WWW-Authenticate challenge pointing at the protected-resource metadata, so MCP clients re-authenticate.
  • Valid token → resolved to the matching account.

The flow is Authorization Code + PKCE, hosted by the server itself:

Endpoint Purpose
GET/POST /oauth/authorize Validates the client/redirect (exact match, https except loopback), renders the two-step login (username, then password or onboarding), redirects with a one-time code. Posting anonymous=1 skips login and issues a code for the anonymous identity.
POST /oauth/token Exchanges code + code_verifier for {access_token, refresh_token}; rotates refresh tokens. Confidential clients authenticate via client_secret_post (form body) or client_secret_basic (HTTP Basic) and may skip PKCE; public clients must use PKCE (S256).
POST /oauth/register RFC 7591 dynamic client registration — post redirect_uris, token_endpoint_auth_method, etc. and get back a client_id (+ client_secret when confidential). Optionally protected by registration_access_token in config.
GET /.well-known/oauth-authorization-server RFC 8414 discovery metadata.
GET /.well-known/oauth-protected-resource RFC 9728 protected-resource metadata.

Only none and client_secret_post are advertised in discovery — Cherry Studio fails the OAuth handshake when client_secret_basic is offered, so DCR-registered clients default to client_secret_post.

Connecting MCP clients

  • Claude Desktop / Cherry Studio / generic — point them at php index.php (stdio) or the HTTP endpoint, and let them complete the interactive browser login at /oauth/authorize.
  • Open WebUI — use the pre-registered confidential client openwebui from config/oauth.php and pick OAuth 2.1 (Static) when adding the MCP server, pasting the client id + secret. The interactive login is the same /oauth/authorize page; its redirect URI must be in openwebui's redirect_uris.
  • Anonymous — POST anonymous=1 to /oauth/authorize (with a valid client/redirect/PKCE) and exchange the resulting code normally; the client connects but only sees/calls public tools.

Passkey (WebAuthn / FIDO2) Authentication

Users can log in passwordlessly with biometrics (Touch ID, Face ID, Windows Hello) or physical security keys (YubiKey):

  • Registration: Log into /account and click "Add Passkey". The browser triggers navigator.credentials.create() and saves the public key in SQLite.
  • Login: Both /account/login and /oauth/authorize support "Sign in with Passkey" and browser WebAuthn Conditional UI (autofill).
  • OAuth 2.1 Compatibility: When an MCP client triggers the OAuth flow, the user authenticates with their passkey in the browser, and the server automatically issues the authorization code back to the MCP client.

Client & Token Housekeeping CLI

SimpleMCP supports Dynamic Client Registration (RFC 7591) while keeping the database clean via a CLI housekeeping tool:

# View dynamically registered clients and expired token stats
php cli/cleanup_clients.php

# Prune expired tokens and inactive dynamic clients (older than 30 days)
php cli/cleanup_clients.php --prune

# Custom threshold (e.g. 7 days) or dry run
php cli/cleanup_clients.php --prune --days=7 --dry-run

Configuration

  • config/users.php — seed users. password is a bcrypt hash; status: 'pending' marks a user for onboarding.
  • config/oauth.php — OAuth clients, token/code TTLs, registration_access_token, and canonical issuer for public deployment.

Project structure

stdio.php                     dedicated stdio JSON-RPC loop for MCP clients
public/
  index.php                   front controller / clean URL router
  bootstrap.php               shared bootstrap entry point
  layout.php                  shared modern UI layout with flash message support
  mcp.php                     standalone Streamable HTTP MCP JSON-RPC API
  sse.php                     standalone Server-Sent Events (SSE) MCP stream
  message.php                 standalone MCP SSE message receiver API
  .well-known/
    oauth-authorization-server.php  RFC 8414 discovery metadata API
    oauth-protected-resource.php    RFC 9728 discovery metadata API
  oauth/
    authorize.php             OAuth 2.1 authorization consent & login page (PRG)
    token.php                 OAuth 2.1 token endpoint API
    register.php              RFC 7591 dynamic client registration API
    passkey/
      options.php             OAuth passkey login options API
      verify.php              OAuth passkey login verify API
  account/
    index.php                 account dashboard page (PRG)
    login.php                 account login page (PRG)
    logout.php                account logout action (PRG)
    onboard.php               account onboarding page (PRG)
    change-password.php       change password action (PRG)
    update-email.php          update email action (PRG)
    2fa/
      verify.php              2FA verification code page (PRG)
      resend.php              2FA code resend action (PRG)
      set-email.php           2FA email setup page (PRG)
    device/
      revoke.php              revoke trusted device action (PRG)
    passkey/
      delete.php              delete passkey action (PRG)
      register/
        options.php           passkey registration options API
        verify.php            passkey registration verify API
      login/
        options.php           passkey login options API
        verify.php            passkey login verify API
cli/
  cleanup_clients.php         housekeeping CLI for dynamic clients and expired tokens
index.php                     root delegator (CLI -> stdio.php, HTTP -> public/index.php)
config/
  users.php                   seed users
  oauth.php                   OAuth clients, TTLs, issuer, registration token
src/
  bootstrap.php               core bootstrap, container, and security/session helpers
  McpServer.php               MCP core: tool registry, routing, access control, UserContext injection
  UserContext.php             immutable user value object (local() / anonymous() factories)
  Attributes/McpFunction.php  the #[McpFunction] attribute
  Tools/                      auto-discovered tool classes
  Auth/
    Database.php              SQLite bootstrap + schema (users, clients, tokens, passkeys)
    UserStore.php             DB-backed accounts: auth, onboarding, change password
    TokenStore.php            OAuth codes/access/refresh tokens (sha256-hashed, single-use, rotating)
    ClientStore.php           OAuth client registry: static config + RFC 7591 dynamic clients
    PasskeyStore.php          WebAuthn passkey credential persistence & counter tracking
    WebAuthn.php              pure-PHP WebAuthn engine (ES256, RS256, CBOR decoding)
    TwoFactorService.php      email 2FA service and trusted device tracking
    EmailService.php          email delivery engine (SMTP, log driver)
    DebugLog.php              append-only diagnostics log
data/                         runtime-only, gitignored (app.sqlite, requests.log, sessions/)

Notes & gotchas

  • data/ is runtime-only and gitignored (/data/*): app.sqlite (users + tokens), requests.log, sessions/. Deleting it re-seeds users from config/users.php on the next HTTP start.
  • Run the dev server with php -S localhost:8000 index.php so data/ is never served statically.
  • The try/catch in processMethod() catches Throwable, so PHP Errors escaping a tool become a -32603 response instead of killing the process.
  • A tools/call response always sets isError: false; tools that return error text in a content block are still reported as successful. Access denials are a JSON-RPC -32001 error, not a tool result.
  • Tool methods receive the unvalidated arguments array — defaults and validation are the tool's responsibility (see CalculatorTool::calculateLoanInstallment).
  • HTTP requests are optionally authenticated: no token → anonymous; present-but-invalid token → 401. To test the full authenticated flow, complete the OAuth login in a browser (or with curl), or log in at /account.
  • The HTTP server is stateless about MCP sessions: it mints an Mcp-Session-Id on initialize (needed by some clients, e.g. Cherry Studio) and echoes it back, but stores nothing.
  • vendor/ contains unused leftovers (phpdotenv, extendorm) not declared in composer.json.

License

This project is licensed under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages