padosoft/askmydocs-mcp-pack

Framework-agnostic MCP (Model Context Protocol) plumbing for Laravel β€” contracts, multi-turn tool-calling orchestrator, stdio client bridge, audit trail, RBAC hooks. Powers AskMyDocs and reusable in any Laravel AI app.

Maintainers

Package info

github.com/padosoft/askmydocs-mcp-pack

pkg:composer/padosoft/askmydocs-mcp-pack

Statistics

Installs: 220

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.4.0 2026-05-15 20:42 UTC

README

Latest Version PHP Version Laravel Version License Tests Total Downloads

AskMyDocs MCP Pack β€” Web Panel Banner

AskMyDocs MCP Pack β€” Admin Dashboard

Framework-agnostic Model Context Protocol plumbing for Laravel. Contracts, multi-turn tool-calling orchestrator, stdio + HTTP transports, audit trail, RBAC hooks. Powers AskMyDocs and reusable in any Laravel AI app.

πŸš€ AI vibe-coding pack included

Every Padosoft package ships a .claude/ folder with curated skills, rules, and commands so Claude Code, Cursor, Copilot, and any other LLM agent can drive the package productively from day one. The pack documents the extension points the framework guarantees as stable (contracts, events, config keys) and the ones that are intentionally private β€” so AI agents stop guessing and start composing.

# Drop into a fresh consumer project
composer require padosoft/askmydocs-mcp-pack
cp -r vendor/padosoft/askmydocs-mcp-pack/.claude ./
# Then ask Claude Code: "wire the orchestrator into my host bridge"

Table of contents

  1. Why this package?
  2. Features at a glance
  3. Screenshots β€” Admin Web Panel
  4. Comparison vs alternatives
  5. Installation
  6. Quick start (3 minutes)
  7. Architecture
  8. Core concepts
  9. Configuration reference
  10. Recipes
  11. Extension points
  12. Testing
  13. Compatibility matrix
  14. Roadmap
  15. Changelog
  16. License

Why this package?

MCP is the open standard Anthropic released in November 2024 for LLM ⇆ tool wire-format interoperability. Within months it was adopted by Cursor, Claude Desktop, VS Code, Cline, Continue, Sourcegraph Cody, OpenAI's Realtime API, and a long tail of editor extensions and agentic frameworks.

What MCP gives you:

  • A JSON-RPC 2.0 contract for initialize, tools/list, tools/call, resources/*, prompts/*.
  • Transport choice β€” stdio (child process) for desktop tools and HTTP/SSE for cloud gateways.
  • A growing public catalog of servers (filesystem, GitHub, Slack, Postgres, Notion, Sentry, …) you can plug into any client.

What MCP does not give you (and what this pack adds):

  • A multi-turn tool-calling loop that drives the model β†’ tools β†’ model β†’ tools cycle with budget caps and audit trail.
  • RBAC / tenant gates in front of every tool invocation.
  • An opinionated audit table with SHA-256 input/output hashes, duration, status, and error excerpts β€” the kind of trail an EU AI-Act audit will ask for.
  • Provider-agnostic integration: the orchestrator does NOT bind the OpenAI / Anthropic / Gemini SDK. You implement a 30-line McpHostBridgeContract against your existing chat manager, and the pack handles the rest.

That is exactly the shape AskMyDocs needed for v7.0. We extracted it so the next Laravel AI app does not have to reinvent it.

Features at a glance

βœ“ Capability
πŸ”Œ Two transports out of the box β€” stdio (Symfony Process) and http (Guzzle via Laravel HTTP client).
🧠 Multi-turn tool-calling orchestrator β€” bounded by max_iterations, with deterministic message reshaping.
πŸ›‘οΈ Tenant-scoped tool catalog β€” forTenant($id) filters by tenant; cross-tenant leakage is structurally impossible.
🚦 Per-call RBAC β€” McpToolAuthorizerContract gates every tool BEFORE it appears in the catalog.
🧾 Hash-only audit trail β€” mcp_tool_call_audit rows store SHA-256 of input + result, NOT raw payloads.
πŸ”„ Cached handshakes β€” initialize + tools/list are cached per (tenant, server) for 5 min by default.
πŸ§ͺ Stub-friendly tests β€” McpClient::useTransportResolver() swaps the transport with a one-line closure.
πŸ“¦ Zero-AI-SDK lock-in β€” pluggable host bridge; works with any provider.
πŸ“Š Production telemetry β€” every tool call carries duration_ms, status, and error excerpt.
🧰 Artisan diagnostics β€” php artisan mcp-pack:ping walks the registry and prints a per-server status table.

Screenshots β€” Admin Web Panel

The companion SPA (padosoft/askmydocs-mcp-pack-admin, post-v7.0 cycle) consumes the v1.4 admin REST routes shipped here. Light + dark themes ship out of the box; every action is keyboard-reachable and audit-logged.

Dashboard

The landing surface β€” fleet health, breaker open-count, recent audit volume per server, and at-a-glance latency percentiles.

Light Dark
Dashboard β€” light Dashboard β€” dark

Servers + per-server detail + tools + circuit breakers

Servers list filters by tenant + transport + status. Drill in for the handshake-cached tool catalog, the most recent audit slice, and the breaker state for every (server, tool) pair β€” read from CircuitBreaker::peekState() so the dashboard NEVER consumes the half-open probe slot just by polling.

Servers list

Server detail

Tools

Circuit breakers

Audit + audit detail + per-server audit

Paginated audit query over the configurable mcp-pack.audit_model, tenant-scoped by default. Filters: server_id, tool_name, status, date range. Click a row for the SHA-256 input/output hashes + redacted error excerpt + duration.

Audit logs

Audit log detail

Per-server audit slice

Prompts + API playground + Settings

Prompt catalog (JSON-RPC prompts/list / prompts/get), an embedded API playground for verifying the routes against the host's auth middleware, and a settings surface mirroring the mcp-pack.* config block.

Prompts

API playground

Settings

Comparison vs alternatives

Feature askmydocs-mcp-pack laravel/mcp (Laravel first-party) php-llm/mcp-sdk (community) Roll-your-own
MCP client support (call upstream) βœ… stdio + http ❌ server-only βœ… stdio + http DIY
MCP server support (expose tools) ⚠️ via host βœ… βœ… DIY
Multi-turn tool-calling loop βœ… ❌ ❌ DIY (~300 LOC)
Provider-agnostic host bridge βœ… n/a ❌ (OpenAI-coupled) DIY
Tenant boundary built-in βœ… forTenant($id) ❌ ❌ DIY
Audit trail with hashes βœ… migration shipped ❌ ❌ DIY (~ADR + migration)
RBAC hook before tool exposure βœ… contract ❌ ❌ DIY (middleware?)
Cached handshake βœ… 5min default ❌ ❌ DIY
Stub transport for tests βœ… one-line closure ❌ partial DIY
.claude/ vibe-coding pack βœ… ❌ ❌ DIY
License MIT MIT MIT n/a

laravel/mcp is excellent for exposing Laravel as an MCP server β€” this pack and laravel/mcp are complementary, not competing. Use both together: laravel/mcp to expose your KB as a server, and this pack to consume other MCP servers from inside your chat flow.

Installation

composer require padosoft/askmydocs-mcp-pack

Publish config + migrations (optional β€” both load automatically):

php artisan vendor:publish --tag=mcp-pack-config
php artisan vendor:publish --tag=mcp-pack-migrations
php artisan migrate

Service provider is auto-discovered via composer.json::extra.laravel.providers.

Quick start (3 minutes)

1. Implement the host bridge

This is the one piece you must write β€” about 30 lines of glue against your existing chat provider:

<?php

namespace App\Mcp;

use App\Ai\AiManager;
use Padosoft\AskMyDocsMcpPack\Contracts\McpHostBridgeContract;
use Padosoft\AskMyDocsMcpPack\Support\HostChatResponse;
use Padosoft\AskMyDocsMcpPack\Support\HostChatTurn;

final class MyHostBridge implements McpHostBridgeContract
{
    public function __construct(private readonly AiManager $ai) {}

    public function chat(HostChatTurn $turn): HostChatResponse
    {
        // Translate $turn->tools into your provider's tool-calling shape.
        $providerTools = array_map(
            fn($tool) => [
                'type' => 'function',
                'function' => [
                    'name' => $tool->name(),
                    'description' => $tool->description(),
                    'parameters' => $tool->schema(),
                ],
            ],
            $turn->tools,
        );

        $response = $this->ai->chatWithHistory('', $turn->messages, [
            'tools' => $providerTools,
            'tool_choice' => 'auto',
        ] + $turn->extras);

        return new HostChatResponse(
            content: $response->content,
            toolCalls: $this->normalizeToolCalls($response->toolCalls),
            provider: $response->provider,
            model: $response->model,
        );
    }

    public function supportsToolCalling(): bool
    {
        return in_array($this->ai->provider()->name(), ['openai', 'openrouter'], true);
    }

    private function normalizeToolCalls(?array $raw): array
    {
        return collect($raw ?? [])->map(fn($c) => [
            'id' => $c['id'],
            'name' => $c['function']['name'] ?? $c['name'],
            'arguments' => is_string($c['function']['arguments'] ?? '')
                ? json_decode($c['function']['arguments'], true) ?? []
                : ($c['arguments'] ?? []),
        ])->all();
    }
}

2. Bind it in AppServiceProvider

use Padosoft\AskMyDocsMcpPack\Contracts\McpHostBridgeContract;

$this->app->singleton(McpHostBridgeContract::class, App\Mcp\MyHostBridge::class);

3. Register at least one MCP server

In-memory (config-driven):

use Padosoft\AskMyDocsMcpPack\Contracts\McpServerRegistryContract;
use Padosoft\AskMyDocsMcpPack\Defaults\InMemoryMcpServer;
use Padosoft\AskMyDocsMcpPack\Defaults\InMemoryMcpServerRegistry;

$this->app->singleton(McpServerRegistryContract::class, function () {
    $registry = new InMemoryMcpServerRegistry();

    $registry->add(new InMemoryMcpServer(
        id: 'fs',
        name: 'Filesystem',
        transport: 'stdio',
        tenantId: null, // platform-global
        transportConfig: [
            'command' => 'npx',
            'args' => ['-y', '@modelcontextprotocol/server-filesystem', '/data'],
            'timeout_ms' => 10_000,
        ],
        allowedTools: ['read_file', 'list_directory'],
    ));

    return $registry;
});

Or back it with your own Eloquent model β€” see Recipes.

4. Drive a chat turn

use Padosoft\AskMyDocsMcpPack\Services\McpToolCallingService;
use Padosoft\AskMyDocsMcpPack\Support\HostMessage;

$svc = app(McpToolCallingService::class);

$response = $svc->chatWithTools(
    messages: [
        HostMessage::system('You are AskMyDocs. Use tools when grounded retrieval helps.'),
        HostMessage::user('What did the deploy runbook change in March?'),
    ],
    tenantId: 'acme',
    actor: auth()->id(),
    context: ['conversation_id' => 42, 'message_id' => 7],
);

return $response->content;

Behind the scenes the orchestrator:

  1. Looks up enabled servers for tenant acme.
  2. Handshakes each one (cached for 5 min).
  3. Filters tools through your McpToolAuthorizerContract.
  4. Hands the catalog to your MyHostBridge::chat().
  5. If the model asks for a tool: invokes it through tools/call, appends the result, and loops back.
  6. Audits every call into mcp_tool_call_audit.

5. Sanity-check

php artisan mcp-pack:ping --tenant=acme
+-----+------------+-----------+--------+--------+---------+-------+
| id  | name       | transport | tenant | status | #tools  | error |
+-----+------------+-----------+--------+--------+---------+-------+
| fs  | Filesystem | stdio     | acme   | ok     | 11      |       |
+-----+------------+-----------+--------+--------+---------+-------+

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Your controller                                                       β”‚
β”‚  └─► McpToolCallingService::chatWithTools()                            β”‚
β”‚        β”‚                                                               β”‚
β”‚        β”œβ”€β–Ί McpServerRegistryContract::forTenant($id)  ─── tenant gate  β”‚
β”‚        β”œβ”€β–Ί McpHandshakeService::refresh()              ─── cached      β”‚
β”‚        β”œβ”€β–Ί McpToolAuthorizerContract::authorize()      ─── RBAC gate   β”‚
β”‚        β”‚                                                               β”‚
β”‚        β”œβ”€β–Ί McpHostBridgeContract::chat($turn)          ─── YOUR CODE   β”‚
β”‚        β”‚       (turn = messages + tool catalog + tenant + extras)      β”‚
β”‚        β”‚                                                               β”‚
β”‚        β”œβ”€β–Ί (loop until model returns no tool_calls or budget hits)     β”‚
β”‚        β”‚                                                               β”‚
β”‚        β”œβ”€β–Ί ToolInvoker::invoke()                                       β”‚
β”‚        β”‚       └─► McpClient::callTool() ── JSON-RPC tools/call ────┐  β”‚
β”‚        β”‚       └─► McpToolCallAudit::create()  ─── audit row           β”‚
β”‚        β”‚                                                               β”‚
β”‚        └─► returns HostChatResponse(content, toolCalls, usage)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                   β”‚
                                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                  β”‚  Upstream MCP server            β”‚
                                                  β”‚  (stdio child process OR        β”‚
                                                  β”‚   HTTP gateway)                 β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Five contracts, three transports, one orchestrator. The blast-radius of swapping any one of them is bounded by the contract.

Core concepts

McpServerContract

A single MCP endpoint your host can talk to. Carries:

  • id() β€” stable identifier scoped per tenant.
  • transport() β€” stdio or http.
  • tenantId() β€” null = platform-global; a string = scoped to that tenant.
  • transportConfig() β€” {command, args, cwd, env} for stdio; {endpoint, headers, timeout_ms} for http.
  • allowedTools() β€” empty array = "all tools the server advertises"; otherwise a per-server allow-list.

Default implementation: InMemoryMcpServer. Production: subclass it on top of your Eloquent model.

McpServerRegistryContract

Per-tenant catalog of McpServerContract entries. The orchestrator always asks forTenant($id) β€” never a global all(). Cross-tenant leakage is structurally impossible.

Default implementation: InMemoryMcpServerRegistry. Production: back it with your own McpServer Eloquent model.

McpHostBridgeContract

The 30-line wrapper around your existing chat manager (OpenAI, Anthropic, OpenRouter, Gemini, …). The pack does NOT bind any AI SDK β€” this is what keeps it provider-agnostic.

McpToolAuthorizerContract

RBAC gate. Called BEFORE the tool appears in the model's catalog, so denied tools never even reach the prompt token budget.

Default implementation: NullMcpToolAuthorizer (allows everything β€” fine for prototypes, MUST be replaced in production).

McpToolContract

The unit of work. Most consumers don't implement this directly β€” RemoteMcpTool is built from the upstream server's tools/list response and used by the orchestrator. You implement it only if you need to expose an in-process tool with no upstream MCP server (uncommon).

Configuration reference

config/mcp-pack.php:

Key Env var Default Purpose
tool_calling.enabled MCP_PACK_TOOL_CALLING_ENABLED false Master kill-switch.
tool_calling.max_iterations MCP_PACK_TOOL_CALLING_MAX_ITERATIONS 3 Hard cap on tool-calling loops per chat turn.
tool_calling.default_tool_choice MCP_PACK_TOOL_CHOICE auto OpenAI-style hint passed to the bridge.
handshake.ttl_seconds MCP_PACK_HANDSHAKE_TTL 300 How long to cache initialize + tools/list.
audit_model MCP_PACK_AUDIT_MODEL McpToolCallAudit::class Override to subclass the audit model.

Recipes

Recipe 1 β€” back the registry with an Eloquent model

final class EloquentMcpServerRegistry implements McpServerRegistryContract
{
    public function forTenant(?string $tenantId): array
    {
        return McpServer::query()
            ->where('tenant_id', $tenantId)
            ->where('enabled', true)
            ->get()
            ->map(fn($m) => new InMemoryMcpServer(
                id: (string) $m->id,
                name: $m->name,
                transport: $m->transport,
                tenantId: $m->tenant_id,
                transportConfig: $m->transport_config ?? [],
                allowedTools: $m->allowed_tools ?? [],
            ))
            ->all();
    }

    public function find(string $id): ?McpServerContract
    {
        $m = McpServer::query()->where('id', $id)->where('enabled', true)->first();
        return $m === null ? null : new InMemoryMcpServer(/* same wrap as above */);
    }
}

$this->app->singleton(McpServerRegistryContract::class, EloquentMcpServerRegistry::class);

Recipe 2 β€” Spatie-permission-backed authorizer

final class SpatieMcpToolAuthorizer implements McpToolAuthorizerContract
{
    public function authorize(mixed $actor, ?string $tenantId, McpToolContract $tool): bool
    {
        if (! $actor instanceof User) { return false; }
        if (! $actor->hasAnyRole(['admin', 'super-admin'])) { return false; }

        $permission = $tool->isReadOnly() ? "mcp.{$tool->name()}.read" : "mcp.{$tool->name()}.write";

        return $actor->hasPermissionTo($permission);
    }
}

Recipe 3 β€” Claude Desktop / Cursor MCP server over stdio

new InMemoryMcpServer(
    id: 'github',
    name: 'GitHub MCP',
    transport: 'stdio',
    tenantId: 'acme',
    transportConfig: [
        'command' => 'npx',
        'args' => ['-y', '@modelcontextprotocol/server-github'],
        'env' => ['GITHUB_PERSONAL_ACCESS_TOKEN' => env('GH_PAT')],
        'timeout_ms' => 15_000,
    ],
    allowedTools: ['search_repositories', 'get_file_contents'],
);

Recipe 4 β€” remote MCP gateway over HTTPS

new InMemoryMcpServer(
    id: 'cloud-kb',
    name: 'Cloud KB Gateway',
    transport: 'http',
    tenantId: 'acme',
    transportConfig: [
        'endpoint' => 'https://mcp.example.com/rpc',
        'headers' => ['Authorization' => 'Bearer ' . env('MCP_TOKEN')],
        'timeout_ms' => 5_000,
        'health_path' => '/healthz',
    ],
);

Recipe 5 β€” coexist with a host-owned audit table

If your host already owns a mcp_tool_call_audit table that pre-dates this pack, the package migration is a no-op (Schema::hasTable('mcp_tool_call_audit') guards both up() and down()). To keep the host's operator-forensics columns (raw redacted payload, user-FK, error blob, …) AND satisfy the package contract, ship ONE additive host migration and one model subclass:

// database/migrations/...add_input_hash_and_actor_to_mcp_tool_call_audit.php
Schema::table('mcp_tool_call_audit', function (Blueprint $table) {
    $table->char('input_hash', 64)->nullable()->after('input_json_redacted');
    $table->string('actor', 100)->nullable()->after('user_id');
    // (also relax any NOT NULL host columns the package does not write)
});

// Backfill existing rows so SHA-256 lookups match pre- and post-pack:
DB::table('mcp_tool_call_audit')
    ->whereNull('input_hash')
    ->orderBy('id')
    ->chunkById(500, function ($rows) {
        foreach ($rows as $row) {
            $payload = is_array($row->input_json_redacted)
                ? json_encode($row->input_json_redacted, JSON_UNESCAPED_UNICODE)
                : $row->input_json_redacted;
            DB::table('mcp_tool_call_audit')
                ->where('id', $row->id)
                ->update(['input_hash' => hash('sha256', (string) $payload)]);
        }
    });
// app/Models/McpToolCallAudit.php β€” subclass + bridging hook
class McpToolCallAudit extends \Padosoft\AskMyDocsMcpPack\Models\McpToolCallAudit
{
    protected $table = 'mcp_tool_call_audit';

    protected $fillable = [
        // package contract
        'tenant_id', 'actor', 'mcp_server_id', 'tool_name',
        'input_hash', 'result_hash', 'duration_ms', 'status', 'error_excerpt',
        // host-legacy columns kept for admin SPA
        'user_id', 'input_json_redacted', 'error_json',
    ];

    protected static function booted(): void
    {
        static::creating(function (self $row) {
            // Bridge actor↔user_id so legacy joins still work.
            if ($row->user_id === null && is_string($row->actor) && ctype_digit($row->actor)) {
                $row->user_id = (int) $row->actor;
            }
            if (($row->actor === null || $row->actor === '') && $row->user_id !== null) {
                $row->actor = (string) $row->user_id;
            }
        });
    }
}
// config/mcp-pack.php β€” point the package at the host subclass
return ['audit_model' => \App\Models\McpToolCallAudit::class];

Now every package ToolInvoker::audit() row fills BOTH schemas; legacy host writes continue to work; the host's existing admin UI and operator-forensics queries keep rendering the same way they always did.

Recipe 6 β€” fail fast under upstream outage (v1.3.0)

Drop in the circuit breaker + retry budget so a flaky MCP server doesn't pin every worker on a long timeout:

# .env β€” opt in, both layers are independent
MCP_PACK_CB_ENABLED=true
MCP_PACK_CB_FAILURE_THRESHOLD=5
MCP_PACK_CB_RECOVERY_SECONDS=30

MCP_PACK_RETRY_ENABLED=true
MCP_PACK_RETRY_MAX_ATTEMPTS=3
MCP_PACK_RETRY_BUCKET_SIZE=20
MCP_PACK_RETRY_BUCKET_WINDOW_SECONDS=60
MCP_PACK_RETRY_BASE_BACKOFF_MS=200
MCP_PACK_RETRY_MAX_BACKOFF_MS=5000
// app/Providers/AppServiceProvider.php β€” wire alerting to the events
use Illuminate\Support\Facades\Event;
use Padosoft\AskMyDocsMcpPack\Resilience\Events\CircuitOpened;
use Padosoft\AskMyDocsMcpPack\Resilience\Events\RetryExhausted;

Event::listen(CircuitOpened::class, function (CircuitOpened $e): void {
    // Page on-call: a server's tool is failing fast.
});

Event::listen(RetryExhausted::class, function (RetryExhausted $e): void {
    // Dashboard tile: which (tenant, server) is burning its budget.
});

ToolInvoker automatically routes through the mediator when either knob is enabled; consumers don't change a line of code. The breaker state is per (server_id, tool_name); the budget is per (tenant_id, server_id) so cross-tenant isolation (R30) holds even under load.

Extension points

Hook Default Override when…
McpHostBridgeContract NullMcpHostBridge (throws) Always β€” wire your provider stack.
McpServerRegistryContract InMemoryMcpServerRegistry You want DB-backed admin UI for server CRUD.
McpToolAuthorizerContract NullMcpToolAuthorizer (allow-all) Always in production β€” wire RBAC + tenant policy.
McpToolCallingService Bound via SP Subclass for custom logging / retry / circuit-breaker logic.
McpHandshakeService Bound via SP Subclass to persist handshakes in a DB column.
McpToolCallAudit Built-in model Subclass + override mcp-pack.audit_model config.
McpClient::useTransportResolver() null (uses transport from server) In tests β€” swap to a stub transport.

Testing

The pack ships its own PHPUnit + Orchestra Testbench setup. To run its tests:

composer install
vendor/bin/phpunit

To test your own host using the pack's stubs:

use Padosoft\AskMyDocsMcpPack\Services\McpClient;
use Padosoft\AskMyDocsMcpPack\Tests\Support\StubMcpTransport;

$transport = (new StubMcpTransport())
    ->scriptInitialize()
    ->scriptListTools([['name' => 'kb_search', 'description' => '...', 'inputSchema' => []]])
    ->scriptToolCall('kb_search', ['hits' => [['title' => 'Doc A']]]);

McpClient::useTransportResolver(fn() => $transport);

// drive your chat flow β€” every JSON-RPC call hits the stub.

End-to-end Playwright coverage in AskMyDocs exercises:

  • chat UI with MCP tools enabled β†’ tool-call summary card renders
  • admin SPA /admin/mcp-servers β†’ server CRUD + ping action
  • audit log shows tool calls with status, duration, server name

Compatibility matrix

PHP Laravel Status
8.3 11.x βœ… tested in CI
8.3 12.x βœ… tested in CI
8.3 13.x βœ… tested in CI
8.4 11.x βœ… tested in CI
8.4 12.x βœ… tested in CI
8.4 13.x βœ… tested in CI
8.5 13.x βœ… tested in CI

Roadmap

Version Status Highlights
v1.0.0 βœ… shipped 2026-05-15 Contracts + orchestrator + stdio/http transports + audit + ping.
v1.0.1 βœ… shipped 2026-05-15 Defensive up()/down() guards on the audit-table migration so the package coexists with a host-owned mcp_tool_call_audit. Recipe 5 walks the coexistence pattern.
v1.1.0 βœ… shipped 2026-05-15 SseJsonRpcTransport for remote HTTP+SSE gateways; JSON-RPC resources/list + resources/read; JSON-RPC prompts/list + prompts/get.
v1.2.0 βœ… shipped 2026-05-15 First-class server-side β€” same package exposes a Laravel app AS an MCP server (stdio long-lived runner + HTTP route + JsonRpcRequestHandler dispatching initialize / tools/list / tools/call / resources/* / prompts/* to a host-supplied catalog + auth + RBAC).
v1.3.0 βœ… shipped 2026-05-15 Per-tool circuit breaker (closed / open / half_open with TTL recovery) + adaptive retry budget (token-bucket per (tenant, server) with exponential backoff capped at maxBackoffMs) + 5 telemetry events. Opt-in, default OFF.
v1.4.0 βœ… shipped 2026-05-15 Admin REST backend β€” read-mostly routes at /api/admin/mcp-pack/{servers,audit,circuit-breaker} registered by the package SP behind MCP_PACK_ADMIN_ENABLED=true. Middleware-driven auth (host wires Sanctum + RBAC). NO React/Vue code β€” this is the backend the separate padosoft/askmydocs-mcp-pack-admin SPA consumes. CRUD writes deferred to v1.5.0 with a writable registry contract.
─ ─ ─
post-v7.0 cycle πŸ“… separate package padosoft/askmydocs-mcp-pack-admin β€” standalone React SPA companion. Same pattern as padosoft/laravel-flow-admin / padosoft/laravel-pii-redactor-admin. Cross-mountable under /admin/mcp/ in any Laravel host that depends on this package + v1.4. Ships in its own repo with its own R36 cycle once AskMyDocs's v7.0/W6 host integration is green.

The v1.1 β†’ v1.4 cycle ships before the AskMyDocs host adopts the package. Consumers willing to ride v1.0 today are welcome to do so β€” the public API surface is stable and won't break before v2 β€” but AskMyDocs's own host integration is intentionally deferred to land over the complete v1.4 feature set (orchestrator + transports + server-side + circuit breaker + admin REST routes) in a single integration cycle rather than four partial passes. See lopadova/AskMyDocs roadmap for the host-side milestones (v7.0/W2 β†’ W6).

Changelog

v1.0.0 β€” planned

  • Initial release extracted from AskMyDocs v6.x.
  • McpToolContract, McpServerContract, McpServerRegistryContract, McpToolAuthorizerContract, McpHostBridgeContract.
  • HttpJsonRpcTransport + StdioJsonRpcTransport.
  • McpToolCallingService multi-turn loop with budget cap.
  • McpHandshakeService with cached handshakes.
  • ToolInvoker with SHA-256 audit trail.
  • mcp_tool_call_audit migration.
  • mcp-pack:ping Artisan diagnostic.
  • NullMcpHostBridge + NullMcpToolAuthorizer + InMemoryMcpServerRegistry defaults.

License

MIT Β© Padosoft. See LICENSE.