Compare commits

...
Sign in to create a new pull request.

34 commits

Author SHA1 Message Date
Snider
5eea244ef3 feat: rename package to lthn/php-mcp for Packagist 2026-03-09 18:00:02 +00:00
Snider
0d7ff83f96 fix: rename core/php-framework dependency to core/php 2026-03-09 17:38:41 +00:00
Snider
1bb0cc9e4f feat: add EaaS shield-check icon to server listings
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:56:43 +00:00
Snider
92e2097022 fix: register MCP layout in layouts:: namespace
- Change <x-layouts.mcp> to <x-layouts::mcp> in all blade views
- Change Livewire #[Layout('components.layouts.mcp')] to layouts::mcp
- Register layouts path via Blade::anonymousComponentPath in Boot
- Layout file already exists at src/Front/View/Blade/layouts/mcp.blade.php

The dot notation couldn't resolve because mcp.blade.php lives in the
package, not the app's components directory. The layouts:: namespace
is the correct pattern matching core-php's component registration.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:38:27 +00:00
Snider
cd82959a0e feat: absorb Front\Mcp frontage from php-framework
Move the MCP frontage ServiceProvider (Core\Front\Mcp\Boot), McpContext
DTO, McpToolHandler interface, and MCP portal blade layout from
php-framework into this package. Namespaces unchanged — added PSR-4
mapping for Core\Front\Mcp\ and auto-discovery provider.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-04 13:05:16 +00:00
Snider
be85428d4c feat: register middleware aliases via lifecycle events
Register mcp.auth, mcp.workspace, mcp.authenticate, mcp.quota, and
mcp.dependencies aliases via $event->middleware() in both
McpRoutesRegistering and ConsoleBooting handlers. Routes now use
the 'mcp.auth' alias instead of the full class name.

Follows the same pattern as php-api (auth.api, api.scope, etc.)
and leverages the framework's processMiddleware() support in all
fire* methods.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-04 12:48:55 +00:00
Snider
8138840cad fix: pass workspace context from API key to tool execution
Brain tools require workspace_id in context for data isolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:41:03 +00:00
Snider
b112b5357a fix: wrap logToolCall in try-catch to prevent cascading failures
Matches the pattern used by logApiRequest and recordQuotaUsage.
Without this, a missing mcp_tool_calls table causes the entire
API response to fail even when tool execution succeeds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:40:01 +00:00
Snider
f37aa26654 feat: replace artisan subprocess with in-process tool execution
Use AgentToolRegistry::execute() directly instead of spawning
php artisan processes. Faster, simpler, and tools are already
registered via McpToolsRegistering lifecycle event.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:38:27 +00:00
Snider
1bf7f2a21c fix: update stale API endpoint URL in playground view
Use request()->getSchemeAndHttpHost()/tools/call instead of
config('app.url')/api/v1/mcp/tools/call — MCP domain is the endpoint.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-04 07:19:56 +00:00
Snider
02c4cc9666 feat: use McpRoutesRegistering for functional MCP routes
Move functional API routes (tools/call, servers.json, etc.) from
Website\Mcp\Routes\web.php to Mcp\Boot via the McpRoutesRegistering
lifecycle event. HTML portal routes stay in web.php with web middleware.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-03 21:52:58 +00:00
Snider
a83d895b25 fix: correct namespaces for MCP controller and middleware
- McpApiController: Mod\Api\Controllers → Core\Mcp\Controllers (matches PSR-4)
- McpApiKeyAuth: Core\Mod\Api\Models\ApiKey → Core\Api\Models\ApiKey
- Route import updated to match

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-03 21:42:42 +00:00
Snider
0653c82148 feat: MCP domain as native endpoint with OpenBrain support
The mcp.* domain now serves both the human-readable portal AND the
functional API. Agents POST to /tools/call with an API key, browsers
get the HTML docs as a fallback. No separate api.* bridge needed.

- Add POST /tools/call, GET /resources/{uri} routes to mcp domain
- Add JSON server list/detail routes (servers.json, servers/{id}.json)
- Add OpenBrain icon to landing, index, and show views
- Replace all api.* domain references with mcp.* (request()->getSchemeAndHttpHost())
- Rewrite connect.blade.php as HTTP-only documentation
- Update ApiExplorer base URL to use current mcp domain
- Remove stdio/Docker configuration from all views

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-03 21:39:36 +00:00
Snider
92faed247e fix: remove hardcoded Host UK branding from MCP portal
- Landing page: "Host UK MCP Ecosystem" → "MCP Ecosystem"
- Landing page: "Host UK infrastructure" → "platform infrastructure via MCP"
- Connect page: remove "Host UK" from setup guide text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 20:50:46 +00:00
Snider
91542fb009 revert: simplify route file back to single config read
Multi-domain logic moved to app-level Boot (wildcard override).

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-03 17:08:51 +00:00
Snider
27e1336b24 feat: multi-domain support for MCP portal routes
Register routes for each domain in config('mcp.domains') array,
allowing the portal to serve from multiple subdomains (e.g.
mcp.host.test and mcp.lthn.test simultaneously).

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-03 16:59:26 +00:00
Snider
547860dd57 fix: correct namespace references in MCP portal routes and controller
Route file referenced Mod\Mcp\ and Website\Mcp\ namespaces which don't
exist — the correct vendor namespaces are Core\Mcp\ and Core\Website\Mcp\.
This was the blocker preventing the MCP portal from loading.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-03 16:51:14 +00:00
Snider
3734b93e14 chore: rename package to core/php-mcp
Aligns composer package name with forge repo path
(forge.lthn.ai/core/php-mcp). Part of host-uk/* → core/* migration.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-03 10:38:58 +00:00
Snider
58d0167e9c fix(ci): install zip in release workflow
Forgejo Composer API requires zip format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:43:59 +00:00
Snider
346504e4fd fix(ci): simplify release workflow, use FORGEJO_REF_NAME
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:36:25 +00:00
Snider
03ffec1221 fix(ci): use Forgejo-native variables in release workflow
Replace github.server_url/GITHUB_REF_NAME with explicit forge URL
and GITEA_REF_NAME/GITEA_OUTPUT.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:13:19 +00:00
Snider
7b6a6539f1 feat: add Forgejo release workflow for Composer registry
On tag push (v*), zips the package and publishes to the
forge.lthn.ai Composer package registry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:00:19 +00:00
7acf59c320 fix(ci): correct container image expression 2026-02-23 13:47:09 +00:00
686deac9b0 feat(ci): use lthn/build:php container image
Replace setup-php action with pre-built container.
Eliminates ~50s setup overhead per matrix job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:46:48 +00:00
Claude
2c766f24af ci: retrigger workflow 2026-02-23 05:48:43 +00:00
Claude
17eae413ec ci: add composer config for path repositories (v5) 2026-02-23 05:45:51 +00:00
Claude
7e962540be
fix(ci): hard-code sister package clone instead of PHP parsing
Direct git clone of ../php-framework avoids shell escaping
issues with dynamic PHP-based path extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 05:31:44 +00:00
Claude
4a048c184e
fix(ci): use single-quoted PHP to avoid shell escaping issues
Switch php -r argument to single quotes so PHP dollar signs
are not interpreted by bash. Pipe output to while-read loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 05:24:26 +00:00
Claude
0d1d4efa94
fix(ci): correct bash escaping in dependency checkout step
The PHP variables inside php -r need \$ escaping, but shell
variables outside need bare $ for command substitution and
variable expansion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 05:19:17 +00:00
Claude
457fecf096
ci: inline workflow to bypass reusable workflow cache
The Forgejo act runner caches reusable workflow definitions,
preventing updates from being picked up. Inline the workflow
with dependency checkout step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 05:11:48 +00:00
Claude
f75d69444e
ci: use reusable PHP test workflow from core/php
Co-Authored-By: Charon <charon@lethean.io>
2026-02-23 01:22:19 +00:00
bcbadf3830 Merge pull request 'chore: discovery scan — 31 improvement issues created (closes #2)' (#35) from chore/issue-2-discovery-scan into main 2026-02-21 01:22:47 +00:00
ef1debf7a9 Merge pull request 'docs(phase-0): environment assessment, findings and phased TODO' (#3) from feat/phase-0-assessment into main 2026-02-21 01:22:30 +00:00
darbs-claude
217e9bbfb6 chore: record discovery scan results for issue #2
Automated scan of the php-mcp codebase identified 30 issues across:
- 12 missing test coverage gaps (services, tools)
- 4 refactoring opportunities (SQL parser, ToolResult DTO, PHPStan, Boot.php)
- 4 infrastructure chores (missing YAML configs, PHPStan setup, CI, streaming)
- 6 feature gaps (templates, schema tools, export, caching, history, validation)
- 3 security reviews required (suspicious query monitoring, ContentTools, commerce)
- 1 documentation gap

All issues created on forge.lthn.ai (issues #4–#34).
Roadmap summary at core/php-mcp#34

Closes #2

Co-Authored-By: darbs-claude <developers@lethean.io>
2026-02-21 01:04:19 +00:00
30 changed files with 760 additions and 223 deletions

57
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,57 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: PHP ${{ matrix.php }}
runs-on: ubuntu-latest
container:
image: lthn/build:php-${{ matrix.php }}
strategy:
fail-fast: true
matrix:
php: ["8.3", "8.4"]
steps:
- uses: actions/checkout@v4
- name: Clone sister packages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Cloning php-framework into ../php-framework"
git clone --depth 1 \
"https://x-access-token:${GITHUB_TOKEN}@forge.lthn.ai/core/php-framework.git" \
../php-framework
ls -la ../php-framework/composer.json
- name: Configure path repositories
run: |
composer config repositories.core path ../php-framework --no-interaction
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run Pint
run: |
if [ -f vendor/bin/pint ]; then
vendor/bin/pint --test
else
echo "Pint not installed, skipping"
fi
- name: Run tests
run: |
if [ -f vendor/bin/pest ]; then
vendor/bin/pest --ci --coverage
elif [ -f vendor/bin/phpunit ]; then
vendor/bin/phpunit --coverage-text
else
echo "No test runner found, skipping"
fi

View file

@ -0,0 +1,38 @@
name: Publish Composer Package
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create package archive
run: |
apt-get update && apt-get install -y zip
zip -r package.zip . \
-x ".forgejo/*" \
-x ".git/*" \
-x "tests/*" \
-x "docker/*" \
-x "*.yaml" \
-x "infection.json5" \
-x "phpstan.neon" \
-x "phpunit.xml" \
-x "psalm.xml" \
-x "rector.php" \
-x "TODO.md" \
-x "ROADMAP.md" \
-x "CONTRIBUTING.md" \
-x "package.json" \
-x "package-lock.json"
- name: Publish to Forgejo Composer registry
run: |
curl --fail --user "${{ secrets.REGISTRY_USER }}:${{ secrets.REGISTRY_TOKEN }}" \
--upload-file package.zip \
"https://forge.lthn.ai/api/packages/core/composer?version=${FORGEJO_REF_NAME#v}"

View file

@ -5,7 +5,7 @@ Model Context Protocol (MCP) tools and analytics for AI-powered automation and i
## Installation
```bash
composer require host-uk/core-mcp
composer require lthn/php-mcp
```
## Features

View file

@ -1,16 +1,23 @@
{
"name": "host-uk/core-mcp",
"name": "lthn/php-mcp",
"description": "MCP (Model Context Protocol) tools module for Core PHP framework",
"keywords": ["laravel", "mcp", "ai", "tools", "claude"],
"keywords": [
"laravel",
"mcp",
"ai",
"tools",
"claude"
],
"license": "EUPL-1.2",
"require": {
"php": "^8.2",
"host-uk/core": "@dev"
"lthn/php": "*"
},
"autoload": {
"psr-4": {
"Core\\Mcp\\": "src/Mcp/",
"Core\\Website\\Mcp\\": "src/Website/Mcp/"
"Core\\Website\\Mcp\\": "src/Website/Mcp/",
"Core\\Front\\Mcp\\": "src/Front/Mcp/"
}
},
"autoload-dev": {
@ -20,9 +27,14 @@
},
"extra": {
"laravel": {
"providers": []
"providers": [
"Core\\Front\\Mcp\\Boot"
]
}
},
"minimum-stability": "stable",
"prefer-stable": true
"prefer-stable": true,
"replace": {
"core/php-mcp": "self.version"
}
}

View file

@ -0,0 +1,52 @@
# Discovery Scan — 2026-02-21
Automated discovery scan performed for issue #2.
## Issues Created
### Test Coverage (12 issues)
- #4 — test: add tests for ToolRegistry service
- #5 — test: add tests for AuditLogService
- #6 — test: add tests for CircuitBreaker service
- #7 — test: add tests for DataRedactor service
- #8 — test: add tests for McpHealthService
- #9 — test: add tests for McpMetricsService
- #10 — test: add tests for McpWebhookDispatcher
- #11 — test: add tests for OpenApiGenerator
- #12 — test: add tests for ToolRateLimiter
- #13 — test: add tests for AgentSessionService
- #14 — test: add tests for AgentToolRegistry
- #15 — test: add integration tests for QueryDatabase tool
### Refactoring (4 issues)
- #16 — refactor: extract SQL parser from regex to AST-based validation
- #17 — refactor: standardise tool responses with ToolResult DTO
- #18 — refactor: fix PHPStan level 5 type errors across services
- #19 — refactor: extract McpToolsRegistering tool registration from McpAgentServerCommand
### Infrastructure / Chores (4 issues)
- #20 — chore: create missing ToolRegistry YAML server definition files
- #21 — chore: add PHPStan and static analysis to dev dependencies
- #22 — chore: add CI/CD security regression tests
- #31 — chore: add query result streaming for large result sets
### Features (6 issues)
- #23 — feat: add query template system
- #24 — feat: add schema exploration tools (ListTables, DescribeTable, ListIndexes)
- #25 — feat: add data export tool (CSV, JSON)
- #26 — feat: add query result caching
- #32 — feat: add query history tracking per workspace
- #33 — feat: add data validation tool for database quality checks
### Security (3 issues)
- #27 — security: add monitoring and alerting for suspicious query patterns
- #28 — security: review ContentTools for injection and data exposure risks
- #29 — security: review commerce tools for payment data exposure
### Documentation (1 issue)
- #30 — docs: add inline documentation for ContentTools and commerce tools
### Roadmap (1 issue)
- #34 — roadmap: php-mcp production readiness
**Total: 31 issues created**

50
src/Front/Mcp/Boot.php Normal file
View file

@ -0,0 +1,50 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Mcp;
use Core\LifecycleEventProvider;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\ServiceProvider;
/**
* MCP frontage - MCP API stage.
*
* Provides mcp middleware group for MCP protocol routes.
* Authentication middleware should be added by the core-mcp package.
*/
class Boot extends ServiceProvider
{
/**
* Configure mcp middleware group.
*/
public static function middleware(Middleware $middleware): void
{
$middleware->group('mcp', [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
}
public function register(): void
{
//
}
public function boot(): void
{
// Fire McpRoutesRegistering event for lazy-loaded modules
LifecycleEventProvider::fireMcpRoutes();
// Fire McpToolsRegistering so modules can register tool handlers
LifecycleEventProvider::fireMcpTools();
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Mcp\Contracts;
use Core\Front\Mcp\McpContext;
/**
* Interface for MCP tool handlers.
*
* Each MCP tool is implemented as a handler class that provides:
* - A JSON schema describing the tool for Claude
* - A handle method that processes tool invocations
*
* Tool handlers are registered via the McpToolsRegistering event
* and can be used by both stdio and HTTP MCP transports.
*/
interface McpToolHandler
{
/**
* Get the JSON schema describing this tool.
*
* The schema follows the MCP tool specification:
* - name: Tool identifier (snake_case)
* - description: What the tool does (for Claude)
* - inputSchema: JSON Schema for parameters
*
* @return array{name: string, description: string, inputSchema: array}
*/
public static function schema(): array;
/**
* Handle a tool invocation.
*
* @param array $args Arguments from the tool call
* @param McpContext $context Server context (session, notifications, etc.)
* @return array Result to return to Claude
*/
public function handle(array $args, McpContext $context): array;
}

View file

@ -0,0 +1,133 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Mcp;
use Closure;
/**
* Context object passed to MCP tool handlers.
*
* Abstracts the transport layer (stdio vs HTTP) so tool handlers
* can work with either transport without modification.
*
* Provides access to:
* - Current session tracking
* - Current plan context
* - Notification sending
* - Session logging
*/
class McpContext
{
/**
* @param object|null $currentPlan AgentPlan model instance when Agentic module installed
*/
public function __construct(
private ?string $sessionId = null,
private ?object $currentPlan = null,
private ?Closure $notificationCallback = null,
private ?Closure $logCallback = null,
) {}
/**
* Get the current session ID if one is active.
*/
public function getSessionId(): ?string
{
return $this->sessionId;
}
/**
* Set the current session ID.
*/
public function setSessionId(?string $sessionId): void
{
$this->sessionId = $sessionId;
}
/**
* Get the current plan if one is active.
*
* @return object|null AgentPlan model instance when Agentic module installed
*/
public function getCurrentPlan(): ?object
{
return $this->currentPlan;
}
/**
* Set the current plan.
*
* @param object|null $plan AgentPlan model instance
*/
public function setCurrentPlan(?object $plan): void
{
$this->currentPlan = $plan;
}
/**
* Send an MCP notification to the client.
*
* Notifications are one-way messages that don't expect a response.
* Common notifications include progress updates, log messages, etc.
*/
public function sendNotification(string $method, array $params = []): void
{
if ($this->notificationCallback) {
($this->notificationCallback)($method, $params);
}
}
/**
* Log a message to the current session.
*
* Messages are recorded in the session log for handoff context
* and audit trail purposes.
*/
public function logToSession(string $message, string $type = 'info', array $data = []): void
{
if ($this->logCallback) {
($this->logCallback)($message, $type, $data);
}
}
/**
* Set the notification callback.
*/
public function setNotificationCallback(?Closure $callback): void
{
$this->notificationCallback = $callback;
}
/**
* Set the log callback.
*/
public function setLogCallback(?Closure $callback): void
{
$this->logCallback = $callback;
}
/**
* Check if a session is currently active.
*/
public function hasSession(): bool
{
return $this->sessionId !== null;
}
/**
* Check if a plan is currently active.
*/
public function hasPlan(): bool
{
return $this->currentPlan !== null;
}
}

View file

@ -0,0 +1,87 @@
@php
$appName = config('core.app.name', 'Core PHP');
$appUrl = config('app.url', 'https://core.test');
$privacyUrl = config('core.urls.privacy', '/privacy');
$termsUrl = config('core.urls.terms', '/terms');
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'MCP Portal' }} - {{ $appName }}</title>
<meta name="description" content="{{ $description ?? 'Connect AI agents via Model Context Protocol' }}">
@vite(['resources/css/app.css', 'resources/js/app.js'])
@fluxAppearance
</head>
<body class="min-h-screen bg-white dark:bg-zinc-900">
<!-- Header -->
<header class="border-b border-zinc-200 dark:border-zinc-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center space-x-8">
<a href="{{ route('mcp.landing') }}" class="flex items-center space-x-2">
<span class="text-xl font-bold text-zinc-900 dark:text-white">MCP Portal</span>
</a>
<nav class="hidden md:flex items-center space-x-6 text-sm">
<a href="{{ route('mcp.servers.index') }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
Servers
</a>
<a href="{{ route('mcp.connect') }}" class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
Setup Guide
</a>
</nav>
</div>
<div class="flex items-center space-x-4">
@php
$workspace = request()->attributes->get('mcp_workspace');
@endphp
@if($workspace)
<a href="{{ route('mcp.dashboard') }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
Dashboard
</a>
<a href="{{ route('mcp.keys') }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
API Keys
</a>
<span class="text-sm text-zinc-500 dark:text-zinc-400">
{{ $workspace->name }}
</span>
@else
<a href="{{ route('login') }}" class="text-sm text-cyan-600 hover:text-cyan-700 dark:text-cyan-400 dark:hover:text-cyan-300">
Sign in
</a>
@endif
<a href="{{ $appUrl }}" class="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white">
&larr; {{ $appName }}
</a>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{{ $slot }}
</main>
<!-- Footer -->
<footer class="border-t border-zinc-200 dark:border-zinc-800 mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between">
<p class="text-sm text-zinc-500 dark:text-zinc-400">
&copy; {{ date('Y') }} {{ $appName }}. All rights reserved.
</p>
<div class="flex items-center space-x-6 text-sm text-zinc-500 dark:text-zinc-400">
<a href="{{ $privacyUrl }}" class="hover:text-zinc-900 dark:hover:text-white">Privacy</a>
<a href="{{ $termsUrl }}" class="hover:text-zinc-900 dark:hover:text-white">Terms</a>
</div>
</div>
</div>
</footer>
@fluxScripts
</body>
</html>

View file

@ -6,6 +6,7 @@ namespace Core\Mcp;
use Core\Events\AdminPanelBooting;
use Core\Events\ConsoleBooting;
use Core\Events\McpRoutesRegistering;
use Core\Events\McpToolsRegistering;
use Core\Mcp\Events\ToolExecuted;
use Core\Mcp\Listeners\RecordToolExecution;
@ -18,6 +19,7 @@ use Core\Mcp\Services\ToolDependencyService;
use Core\Mcp\Services\ToolRegistry;
use Core\Mcp\Services\ToolVersionService;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class Boot extends ServiceProvider
@ -35,6 +37,7 @@ class Boot extends ServiceProvider
public static array $listens = [
AdminPanelBooting::class => 'onAdminPanel',
ConsoleBooting::class => 'onConsole',
McpRoutesRegistering::class => 'onMcpRoutes',
McpToolsRegistering::class => 'onMcpTools',
];
@ -87,8 +90,42 @@ class Boot extends ServiceProvider
$event->livewire('mcp.admin.tool-version-manager', View\Modal\Admin\ToolVersionManager::class);
}
public function onMcpRoutes(McpRoutesRegistering $event): void
{
// Register middleware aliases
$event->middleware('mcp.auth', Middleware\McpApiKeyAuth::class);
$event->middleware('mcp.workspace', Middleware\ValidateWorkspaceContext::class);
$event->middleware('mcp.authenticate', Middleware\McpAuthenticate::class);
$event->middleware('mcp.quota', Middleware\CheckMcpQuota::class);
$event->middleware('mcp.dependencies', Middleware\ValidateToolDependencies::class);
$domain = config('mcp.domain');
$event->routes(fn () => Route::domain($domain)
->middleware('mcp.auth')
->name('mcp.')
->group(function () {
Route::post('tools/call', [Controllers\McpApiController::class, 'callTool'])->name('tools.call');
Route::get('resources/{uri}', [Controllers\McpApiController::class, 'resource'])->name('resources.read')
->where('uri', '.+');
Route::get('servers.json', [Controllers\McpApiController::class, 'servers'])->name('servers.json');
Route::get('servers/{id}.json', [Controllers\McpApiController::class, 'server'])->name('servers.json.show')
->where('id', '[a-z0-9-]+');
Route::get('servers/{id}/tools', [Controllers\McpApiController::class, 'tools'])->name('servers.tools')
->where('id', '[a-z0-9-]+');
})
);
}
public function onConsole(ConsoleBooting $event): void
{
// Middleware aliases for CLI context (artisan route:list etc.)
$event->middleware('mcp.auth', Middleware\McpApiKeyAuth::class);
$event->middleware('mcp.workspace', Middleware\ValidateWorkspaceContext::class);
$event->middleware('mcp.authenticate', Middleware\McpAuthenticate::class);
$event->middleware('mcp.quota', Middleware\CheckMcpQuota::class);
$event->middleware('mcp.dependencies', Middleware\ValidateToolDependencies::class);
$event->command(Console\Commands\McpAgentServerCommand::class);
$event->command(Console\Commands\PruneMetricsCommand::class);
$event->command(Console\Commands\VerifyAuditLogCommand::class);

View file

@ -2,14 +2,14 @@
declare(strict_types=1);
namespace Mod\Api\Controllers;
namespace Core\Mcp\Controllers;
use Core\Front\Controller;
use Core\Mcp\Services\McpQuotaService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Mod\Api\Models\ApiKey;
use Core\Api\Models\ApiKey;
use Core\Mcp\Models\McpApiRequest;
use Core\Mcp\Models\McpToolCall;
use Core\Mcp\Services\McpWebhookDispatcher;
@ -119,11 +119,11 @@ class McpApiController extends Controller
$startTime = microtime(true);
try {
// Execute the tool via artisan command
$result = $this->executeToolViaArtisan(
$validated['server'],
// Execute the tool via in-process registry or artisan fallback
$result = $this->executeTool(
$validated['tool'],
$validated['arguments'] ?? []
$validated['arguments'] ?? [],
$apiKey
);
$durationMs = (int) ((microtime(true) - $startTime) * 1000);
@ -201,60 +201,40 @@ class McpApiController extends Controller
}
/**
* Execute tool via artisan MCP server command.
* Execute a tool via the in-process AgentToolRegistry.
*
* Tools are registered at boot via the McpToolsRegistering lifecycle event.
* This avoids the overhead of spawning artisan sub-processes for each call.
*
* @throws \RuntimeException If tool not found in registry
*/
protected function executeToolViaArtisan(string $server, string $tool, array $arguments): mixed
protected function executeTool(string $tool, array $arguments, ?ApiKey $apiKey): mixed
{
$commandMap = config('api.mcp.server_commands', []);
$registryClass = \Core\Mod\Agentic\Services\AgentToolRegistry::class;
$command = $commandMap[$server] ?? null;
if (! $command) {
throw new \RuntimeException("Unknown server: {$server}");
if (! app()->bound($registryClass)) {
throw new \RuntimeException("AgentToolRegistry not available — is the agentic module installed?");
}
// Build MCP request
$mcpRequest = [
'jsonrpc' => '2.0',
'id' => uniqid(),
'method' => 'tools/call',
'params' => [
'name' => $tool,
'arguments' => $arguments,
],
];
$registry = app($registryClass);
// Execute via process
$process = proc_open(
['php', 'artisan', $command],
[
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
],
$pipes,
base_path()
if (! $registry->has($tool)) {
throw new \RuntimeException("Tool not found: {$tool}");
}
$context = [];
if ($apiKey?->workspace_id) {
$context['workspace_id'] = $apiKey->workspace_id;
}
return $registry->execute(
name: $tool,
args: $arguments,
context: $context,
apiKey: $apiKey,
validateDependencies: false
);
if (! is_resource($process)) {
throw new \RuntimeException('Failed to start MCP server process');
}
fwrite($pipes[0], json_encode($mcpRequest)."\n");
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
$response = json_decode($output, true);
if (isset($response['error'])) {
throw new \RuntimeException($response['error']['message'] ?? 'Tool execution failed');
}
return $response['result'] ?? null;
}
/**
@ -333,15 +313,20 @@ class McpApiController extends Controller
bool $success,
?string $error = null
): void {
McpToolCall::log(
serverId: $request['server'],
toolName: $request['tool'],
params: $request['arguments'] ?? [],
success: $success,
durationMs: $durationMs,
errorMessage: $error,
workspaceId: $apiKey?->workspace_id
);
try {
McpToolCall::log(
serverId: $request['server'],
toolName: $request['tool'],
params: $request['arguments'] ?? [],
success: $success,
durationMs: $durationMs,
errorMessage: $error,
workspaceId: $apiKey?->workspace_id
);
} catch (\Throwable $e) {
// Don't let logging failures affect API response
report($e);
}
}
/**

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Core\Mcp\Middleware;
use Core\Mod\Api\Models\ApiKey;
use Core\Api\Models\ApiKey;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Website\Mcp;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
@ -25,6 +26,11 @@ class Boot extends ServiceProvider
{
$this->loadViewsFrom(__DIR__.'/View/Blade', 'mcp');
// Register mcp layout into the layouts:: namespace
$layoutsPath = dirname(__DIR__, 2).'/Front/View/Blade/layouts';
$this->loadViewsFrom($layoutsPath, 'layouts');
Blade::anonymousComponentPath($layoutsPath, 'layouts');
$this->registerLivewireComponents();
$this->registerRoutes();
}
@ -43,6 +49,7 @@ class Boot extends ServiceProvider
protected function registerRoutes(): void
{
// HTML portal routes need web middleware (sessions, CSRF for Livewire)
Route::middleware('web')->group(__DIR__.'/Routes/web.php');
}
}

View file

@ -7,8 +7,8 @@ namespace Core\Website\Mcp\Controllers;
use Core\Front\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Mod\Mcp\Models\McpToolCall;
use Mod\Mcp\Services\OpenApiGenerator;
use Core\Mcp\Models\McpToolCall;
use Core\Mcp\Services\OpenApiGenerator;
use Symfony\Component\Yaml\Yaml;
/**

View file

@ -1,43 +1,41 @@
<?php
use Illuminate\Support\Facades\Route;
use Mod\Mcp\Middleware\McpAuthenticate;
use Website\Mcp\Controllers\McpRegistryController;
use Core\Mcp\Middleware\McpAuthenticate;
use Core\Website\Mcp\Controllers\McpRegistryController;
/*
|--------------------------------------------------------------------------
| MCP Portal Routes (mcp.host.uk.com)
| MCP Portal Routes (HTML)
|--------------------------------------------------------------------------
|
| Public routes for the MCP server registry and documentation portal.
| These routes serve both human-readable docs and machine-readable JSON.
| Human-readable documentation portal for the MCP domain.
| Wrapped in 'web' middleware by Website\Mcp\Boot for sessions/CSRF (Livewire).
|
| Functional API routes (tools/call, servers.json, etc.) are registered
| via the McpRoutesRegistering lifecycle event in Core\Mcp\Boot.
|
*/
$mcpDomain = config('mcp.domain', 'mcp.host.uk.com');
Route::domain($mcpDomain)->name('mcp.')->group(function () {
// Agent discovery endpoint (always JSON)
Route::domain(config('mcp.domain'))->name('mcp.')->group(function () {
// Agent discovery endpoint (always JSON, no auth)
Route::get('.well-known/mcp-servers.json', [McpRegistryController::class, 'registry'])
->name('registry');
// Landing page
// ── Human-readable portal (optional auth) ────────────────────
Route::get('/', [McpRegistryController::class, 'landing'])
->middleware(McpAuthenticate::class.':optional')
->name('landing');
// Server list (HTML/JSON based on Accept header)
Route::get('servers', [McpRegistryController::class, 'index'])
->middleware(McpAuthenticate::class.':optional')
->name('servers.index');
// Server detail (supports .json extension)
Route::get('servers/{id}', [McpRegistryController::class, 'show'])
->middleware(McpAuthenticate::class.':optional')
->name('servers.show')
->where('id', '[a-z0-9-]+(?:\.json)?');
->where('id', '[a-z0-9-]+');
// Connection config page
Route::get('connect', [McpRegistryController::class, 'connect'])
->middleware(McpAuthenticate::class.':optional')
->name('connect');

View file

@ -1,4 +1,4 @@
<x-layouts.mcp>
<x-layouts::mcp>
<x-slot:title>{{ $server['name'] }} Analytics</x-slot:title>
<div class="mb-8">
@ -112,4 +112,4 @@
@endforeach
</flux:button.group>
</div>
</x-layouts.mcp>
</x-layouts::mcp>

View file

@ -26,8 +26,8 @@
wire:model="baseUrl"
class="rounded-md border-yellow-300 shadow-sm focus:border-yellow-500 focus:ring-yellow-500 text-sm"
>
<option value="https://api.host.uk.com">Production</option>
<option value="https://api.staging.host.uk.com">Staging</option>
<option value="https://mcp.lthn.ai">Production</option>
<option value="https://mcp.lthn.sh">Homelab</option>
<option value="http://localhost">Local</option>
</select>
</div>

View file

@ -149,13 +149,14 @@
<p class="text-zinc-600 dark:text-zinc-400 mb-4 text-sm">
Call an MCP tool via HTTP POST:
</p>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-3 overflow-x-auto text-xs"><code class="text-emerald-400">curl -X POST https://mcp.host.uk.com/api/v1/tools/call \
@php $mcpUrl = request()->getSchemeAndHttpHost(); @endphp
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-3 overflow-x-auto text-xs"><code class="text-emerald-400">curl -X POST {{ $mcpUrl }}/tools/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"server": "commerce",
"tool": "product_list",
"arguments": {}
"server": "openbrain",
"tool": "brain_recall",
"arguments": { "query": "recent decisions" }
}'</code></pre>
</div>
</div>

View file

@ -1,11 +1,15 @@
<x-layouts.mcp>
<x-layouts::mcp>
<x-slot:title>Setup Guide</x-slot:title>
@php
$mcpUrl = request()->getSchemeAndHttpHost();
@endphp
<div class="max-w-4xl mx-auto">
<div class="mb-8">
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white">Setup Guide</h1>
<p class="mt-2 text-lg text-zinc-600 dark:text-zinc-400">
Connect to Host UK MCP servers via HTTP API or stdio.
Connect AI agents to MCP servers via HTTP.
</p>
</div>
@ -28,7 +32,7 @@
</a>
</div>
<!-- HTTP API (Primary) -->
<!-- HTTP API -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border-2 border-cyan-500 p-6 mb-8">
<div class="flex items-center space-x-3 mb-4">
<div class="p-2 bg-cyan-100 dark:bg-cyan-900/30 rounded-lg">
@ -37,143 +41,118 @@
<div>
<h2 class="text-xl font-semibold text-zinc-900 dark:text-white">HTTP API</h2>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300">
Recommended
All platforms
</span>
</div>
</div>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Call MCP tools from any language or platform using standard HTTP requests.
Perfect for external integrations, webhooks, and remote agents.
<p class="text-zinc-600 dark:text-zinc-400 mb-6">
Call MCP tools from any language, platform, or AI agent using standard HTTP requests.
Works with Claude Code, Cursor, custom agents, webhooks, and any HTTP client.
</p>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">1. Get your API key</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-4 text-sm">
Sign in to your Host UK account to create an API key from the admin dashboard.
Create an API key from your admin dashboard. Keys use the <code class="px-1 py-0.5 bg-zinc-100 dark:bg-zinc-700 rounded text-xs">hk_</code> prefix.
</p>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">2. Call a tool</h3>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm mb-4"><code class="text-emerald-400">curl -X POST https://mcp.host.uk.com/api/v1/mcp/tools/call \
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">2. Discover available servers</h3>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm mb-4"><code class="text-emerald-400">curl {{ $mcpUrl }}/servers.json \
-H "Authorization: Bearer YOUR_API_KEY"</code></pre>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">3. Call a tool</h3>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm mb-4"><code class="text-emerald-400">curl -X POST {{ $mcpUrl }}/tools/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"server": "commerce",
"tool": "product_list",
"arguments": { "category": "hosting" }
"server": "openbrain",
"tool": "brain_recall",
"arguments": { "query": "authentication decisions" }
}'</code></pre>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">3. List available tools</h3>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-emerald-400">curl https://mcp.host.uk.com/api/v1/mcp/servers \
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">4. Read a resource</h3>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-emerald-400">curl {{ $mcpUrl }}/resources/plans://all \
-H "Authorization: Bearer YOUR_API_KEY"</code></pre>
<div class="mt-6 p-4 bg-zinc-50 dark:bg-zinc-900/50 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300">API Endpoints</h4>
<h4 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300">Endpoints</h4>
<a href="{{ route('mcp.openapi.json') }}" target="_blank" class="text-xs text-cyan-600 hover:text-cyan-700 dark:text-cyan-400">
View OpenAPI Spec
</a>
</div>
<div class="space-y-2 text-sm">
<div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">GET /api/v1/mcp/servers</code>
<code class="text-zinc-600 dark:text-zinc-400">GET /.well-known/mcp-servers.json</code>
<span class="text-zinc-500">Agent discovery</span>
</div>
<div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">GET /servers</code>
<span class="text-zinc-500">List all servers</span>
</div>
<div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">GET /api/v1/mcp/servers/{id}</code>
<span class="text-zinc-500">Server details</span>
<code class="text-zinc-600 dark:text-zinc-400">GET /servers/{id}</code>
<span class="text-zinc-500">Server details + tools</span>
</div>
<div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">GET /api/v1/mcp/servers/{id}/tools</code>
<span class="text-zinc-500">List tools</span>
</div>
<div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">POST /api/v1/mcp/tools/call</code>
<code class="text-zinc-600 dark:text-zinc-400">POST /tools/call</code>
<span class="text-zinc-500">Execute a tool</span>
</div>
<div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">GET /api/v1/mcp/resources/{uri}</code>
<code class="text-zinc-600 dark:text-zinc-400">GET /resources/{uri}</code>
<span class="text-zinc-500">Read a resource</span>
</div>
</div>
</div>
</div>
<!-- Stdio (Secondary) -->
<!-- Code Examples -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 p-6 mb-8">
<div class="flex items-center space-x-3 mb-4">
<div class="p-2 bg-violet-100 dark:bg-violet-900/30 rounded-lg">
<flux:icon.command-line class="w-6 h-6 text-violet-600 dark:text-violet-400" />
<flux:icon.code-bracket class="w-6 h-6 text-violet-600 dark:text-violet-400" />
</div>
<div>
<h2 class="text-xl font-semibold text-zinc-900 dark:text-white">Stdio (Local)</h2>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-zinc-100 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-400">
For local development
</span>
<h2 class="text-xl font-semibold text-zinc-900 dark:text-white">Code Examples</h2>
</div>
</div>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Direct stdio connection for Claude Code and other local AI agents.
Ideal for OSS framework users running their own Host Hub instance.
</p>
<div class="space-y-6">
<!-- Python -->
<div>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">Python</h3>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-emerald-400">import requests
<details class="group">
<summary class="cursor-pointer text-sm font-medium text-cyan-600 dark:text-cyan-400 hover:text-cyan-700">
Show stdio configuration
</summary>
<div class="mt-4 space-y-6">
<!-- Claude Code -->
<div>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">Claude Code</h3>
<p class="text-zinc-500 dark:text-zinc-400 text-sm mb-2">
Add to <code class="px-1 py-0.5 bg-zinc-100 dark:bg-zinc-700 rounded">~/.claude/claude_code_config.json</code>:
</p>
<pre class="bg-zinc-100 dark:bg-zinc-900 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-zinc-800 dark:text-zinc-200">{
"mcpServers": {
@foreach($servers as $server)
"{{ $server['id'] }}": {
"command": "{{ $server['connection']['command'] ?? 'php' }}",
"args": {!! json_encode($server['connection']['args'] ?? ['artisan', 'mcp:agent-server']) !!},
"cwd": "{{ $server['connection']['cwd'] ?? '/path/to/host.uk.com' }}"
}{{ !$loop->last ? ',' : '' }}
@endforeach
}
}</code></pre>
</div>
<!-- Cursor -->
<div>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">Cursor</h3>
<p class="text-zinc-500 dark:text-zinc-400 text-sm mb-2">
Add to <code class="px-1 py-0.5 bg-zinc-100 dark:bg-zinc-700 rounded">.cursor/mcp.json</code>:
</p>
<pre class="bg-zinc-100 dark:bg-zinc-900 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-zinc-800 dark:text-zinc-200">{
"mcpServers": {
@foreach($servers as $server)
"{{ $server['id'] }}": {
"command": "{{ $server['connection']['command'] ?? 'php' }}",
"args": {!! json_encode($server['connection']['args'] ?? ['artisan', 'mcp:agent-server']) !!}
}{{ !$loop->last ? ',' : '' }}
@endforeach
}
}</code></pre>
</div>
<!-- Docker -->
<div>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">Docker</h3>
<pre class="bg-zinc-100 dark:bg-zinc-900 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-zinc-800 dark:text-zinc-200">{
"mcpServers": {
"hosthub-agent": {
"command": "docker",
"args": ["exec", "-i", "hosthub-app", "php", "artisan", "mcp:agent-server"]
resp = requests.post(
"{{ $mcpUrl }}/tools/call",
headers={"Authorization": "Bearer hk_your_key"},
json={
"server": "openbrain",
"tool": "brain_recall",
"arguments": {"query": "recent decisions"}
}
}
}</code></pre>
</div>
)
print(resp.json())</code></pre>
</div>
</details>
<!-- JavaScript -->
<div>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">JavaScript</h3>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-emerald-400">const resp = await fetch("{{ $mcpUrl }}/tools/call", {
method: "POST",
headers: {
"Authorization": "Bearer hk_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
server: "openbrain",
tool: "brain_recall",
arguments: { query: "recent decisions" },
}),
});
const data = await resp.json();</code></pre>
</div>
</div>
</div>
<!-- Authentication Methods -->
@ -199,6 +178,28 @@
check your key's server scopes in your admin dashboard.
</p>
</div>
<div class="mt-4 p-4 bg-cyan-50 dark:bg-cyan-900/20 rounded-lg border border-cyan-200 dark:border-cyan-800">
<h4 class="text-sm font-semibold text-cyan-800 dark:text-cyan-300 mb-1">Rate limiting</h4>
<p class="text-sm text-cyan-700 dark:text-cyan-400">
Requests are rate limited to 120 per minute. Rate limit headers
(<code class="text-xs">X-RateLimit-Limit</code>, <code class="text-xs">X-RateLimit-Remaining</code>)
are included in all responses.
</p>
</div>
</div>
<!-- Discovery -->
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 p-6 mb-8">
<h2 class="text-xl font-semibold text-zinc-900 dark:text-white mb-4">Discovery</h2>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Agents discover available servers automatically via the well-known endpoint:
</p>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm mb-4"><code class="text-emerald-400">curl {{ $mcpUrl }}/.well-known/mcp-servers.json</code></pre>
<p class="text-sm text-zinc-500 dark:text-zinc-400">
Returns the server registry with capabilities and connection details.
No authentication required for discovery.
</p>
</div>
<!-- Help -->
@ -208,10 +209,10 @@
<flux:button href="{{ route('mcp.servers.index') }}" icon="server-stack">
Browse Servers
</flux:button>
<flux:button href="https://host.uk.com/contact" variant="ghost">
Contact Support
<flux:button href="{{ route('mcp.openapi.json') }}" icon="code-bracket" variant="ghost" target="_blank">
OpenAPI Spec
</flux:button>
</div>
</div>
</div>
</x-layouts.mcp>
</x-layouts::mcp>

View file

@ -1,4 +1,4 @@
<x-layouts.mcp>
<x-layouts::mcp>
<x-slot:title>MCP Servers</x-slot:title>
<div class="mb-8">
@ -32,9 +32,15 @@
@case('supporthost')
<flux:icon.chat-bubble-left-right class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('openbrain')
<flux:icon.light-bulb class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('analyticshost')
<flux:icon.chart-bar class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('eaas')
<flux:icon.shield-check class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@default
<flux:icon.server class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@endswitch
@ -123,4 +129,4 @@
</div>
</div>
@endif
</x-layouts.mcp>
</x-layouts::mcp>

View file

@ -1,6 +1,6 @@
<x-layouts.mcp>
<x-layouts::mcp>
<x-slot:title>API Keys</x-slot:title>
<x-slot:description>Manage API keys for MCP server access.</x-slot:description>
<livewire:mcp.api-key-manager :workspace="$workspace" />
</x-layouts.mcp>
</x-layouts::mcp>

View file

@ -1,14 +1,14 @@
<x-layouts.mcp>
<x-layouts::mcp>
<x-slot:title>MCP Portal</x-slot:title>
<x-slot:description>Connect AI agents to Host UK infrastructure. Machine-readable, agent-optimised, human-friendly.</x-slot:description>
<x-slot:description>Connect AI agents to platform infrastructure via Model Context Protocol. Machine-readable, agent-optimised, human-friendly.</x-slot:description>
<!-- Hero -->
<div class="text-center mb-16">
<h1 class="text-4xl font-bold text-zinc-900 dark:text-white mb-4">
Host UK MCP Ecosystem
MCP Ecosystem
</h1>
<p class="text-xl text-zinc-600 dark:text-zinc-400 max-w-2xl mx-auto mb-8">
Connect AI agents to Host UK infrastructure.<br>
Connect AI agents to platform infrastructure via MCP.<br>
<span class="text-cyan-600 dark:text-cyan-400">Machine-readable</span> &bull;
<span class="text-cyan-600 dark:text-cyan-400">Agent-optimised</span> &bull;
<span class="text-cyan-600 dark:text-cyan-400">Human-friendly</span>
@ -93,9 +93,15 @@
@case('supporthost')
<flux:icon.chat-bubble-left-right class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('openbrain')
<flux:icon.light-bulb class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('analyticshost')
<flux:icon.chart-bar class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('eaas')
<flux:icon.shield-check class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@default
<flux:icon.server class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@endswitch
@ -180,18 +186,21 @@
@endif
<!-- Quick Start -->
@php
$mcpUrl = request()->getSchemeAndHttpHost();
@endphp
<section class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 p-8">
<h2 class="text-2xl font-semibold text-zinc-900 dark:text-white mb-4">Quick Start</h2>
<p class="text-zinc-600 dark:text-zinc-400 mb-6">
Call MCP tools via HTTP API with your API key:
Call MCP tools via HTTP with your API key:
</p>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm mb-6"><code class="text-emerald-400">curl -X POST https://mcp.host.uk.com/api/v1/mcp/tools/call \
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm mb-6"><code class="text-emerald-400">curl -X POST {{ $mcpUrl }}/tools/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"server": "commerce",
"tool": "product_list",
"arguments": {}
"server": "openbrain",
"tool": "brain_recall",
"arguments": { "query": "recent decisions" }
}'</code></pre>
<div class="flex flex-wrap items-center gap-4">
<flux:button href="{{ route('mcp.connect') }}" icon="document-text" variant="primary">
@ -202,4 +211,4 @@
</flux:button>
</div>
</section>
</x-layouts.mcp>
</x-layouts::mcp>

View file

@ -236,7 +236,7 @@
<div class="space-y-3 text-sm">
<div>
<span class="text-zinc-500 dark:text-zinc-400">Endpoint:</span>
<code class="ml-2 text-zinc-800 dark:text-zinc-200 break-all">{{ config('app.url') }}/api/v1/mcp/tools/call</code>
<code class="ml-2 text-zinc-800 dark:text-zinc-200 break-all">{{ request()->getSchemeAndHttpHost() }}/tools/call</code>
</div>
<div>
<span class="text-zinc-500 dark:text-zinc-400">Method:</span>

View file

@ -1,4 +1,4 @@
<x-layouts.mcp>
<x-layouts::mcp>
<x-slot:title>{{ $server['name'] }}</x-slot:title>
<x-slot:description>{{ $server['tagline'] ?? $server['description'] ?? '' }}</x-slot:description>
@ -29,9 +29,15 @@
@case('supporthost')
<flux:icon.chat-bubble-left-right class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break
@case('openbrain')
<flux:icon.light-bulb class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break
@case('analyticshost')
<flux:icon.chart-bar class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break
@case('eaas')
<flux:icon.shield-check class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break
@case('upstream')
<flux:icon.arrows-up-down class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break
@ -96,18 +102,28 @@
</div>
<!-- Connection -->
@if(!empty($server['connection']))
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 p-6 mb-8">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white mb-4">Connection</h2>
<pre class="bg-zinc-100 dark:bg-zinc-900 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-zinc-800 dark:text-zinc-200">{
"{{ $server['id'] }}": {
"command": "{{ $server['connection']['command'] ?? 'php' }}",
"args": {!! json_encode($server['connection']['args'] ?? ['artisan', 'mcp:agent-server']) !!},
"cwd": "{{ $server['connection']['cwd'] ?? '/path/to/project' }}"
}
}</code></pre>
</div>
@endif
@php
$mcpUrl = request()->getSchemeAndHttpHost();
@endphp
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 p-6 mb-8">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white mb-4">Connection</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
Call tools on this server via HTTP:
</p>
<pre class="bg-zinc-900 dark:bg-zinc-950 rounded-lg p-4 overflow-x-auto text-sm"><code class="text-emerald-400">curl -X POST {{ $mcpUrl }}/tools/call \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"server": "{{ $server['id'] }}",
"tool": "{{ !empty($server['tools']) ? $server['tools'][0]['name'] : 'tool_name' }}",
"arguments": {}
}'</code></pre>
<p class="mt-3 text-xs text-zinc-500 dark:text-zinc-400">
<a href="{{ route('mcp.connect') }}" class="text-cyan-600 dark:text-cyan-400 hover:underline">
Full setup guide
</a>
</p>
</div>
<!-- Tools -->
@if(!empty($server['tools']))
@ -224,4 +240,4 @@
View Usage Analytics
</a>
</div>
</x-layouts.mcp>
</x-layouts::mcp>

View file

@ -108,8 +108,8 @@ class ApiExplorer extends Component
public function mount(): void
{
// Set base URL from config
$this->baseUrl = config('api.base_url', config('app.url'));
// Set base URL from current request (mcp domain)
$this->baseUrl = request()->getSchemeAndHttpHost();
// Pre-select first endpoint
if (! empty($this->endpoints)) {

View file

@ -13,7 +13,7 @@ use Core\Mcp\Services\McpMetricsService;
*
* Displays analytics and metrics for MCP tool usage.
*/
#[Layout('components.layouts.mcp')]
#[Layout('layouts::mcp')]
class McpMetrics extends Component
{
public int $days = 7;

View file

@ -18,7 +18,7 @@ use Symfony\Component\Yaml\Yaml;
* A browser-based UI for testing MCP tool calls.
* Allows users to select a server, pick a tool, and execute it with custom parameters.
*/
#[Layout('components.layouts.mcp')]
#[Layout('layouts::mcp')]
class McpPlayground extends Component
{
public string $selectedServer = '';

View file

@ -14,7 +14,7 @@ use Symfony\Component\Yaml\Yaml;
/**
* MCP Playground - interactive tool testing in the browser.
*/
#[Layout('components.layouts.mcp')]
#[Layout('layouts::mcp')]
class Playground extends Component
{
public string $selectedServer = '';

View file

@ -12,7 +12,7 @@ use Core\Mcp\Models\McpApiRequest;
/**
* MCP Request Log - view and replay API requests.
*/
#[Layout('components.layouts.mcp')]
#[Layout('layouts::mcp')]
class RequestLog extends Component
{
use WithPagination;

View file

@ -15,7 +15,7 @@ use Livewire\Component;
* Single search interface across all system components:
* MCP tools, API endpoints, patterns, assets, todos, and plans.
*/
#[Layout('components.layouts.mcp')]
#[Layout('layouts::mcp')]
class UnifiedSearch extends Component
{
public string $query = '';