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 ## Installation
```bash ```bash
composer require host-uk/core-mcp composer require lthn/php-mcp
``` ```
## Features ## 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", "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", "license": "EUPL-1.2",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"host-uk/core": "@dev" "lthn/php": "*"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Core\\Mcp\\": "src/Mcp/", "Core\\Mcp\\": "src/Mcp/",
"Core\\Website\\Mcp\\": "src/Website/Mcp/" "Core\\Website\\Mcp\\": "src/Website/Mcp/",
"Core\\Front\\Mcp\\": "src/Front/Mcp/"
} }
}, },
"autoload-dev": { "autoload-dev": {
@ -20,9 +27,14 @@
}, },
"extra": { "extra": {
"laravel": { "laravel": {
"providers": [] "providers": [
"Core\\Front\\Mcp\\Boot"
]
} }
}, },
"minimum-stability": "stable", "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\AdminPanelBooting;
use Core\Events\ConsoleBooting; use Core\Events\ConsoleBooting;
use Core\Events\McpRoutesRegistering;
use Core\Events\McpToolsRegistering; use Core\Events\McpToolsRegistering;
use Core\Mcp\Events\ToolExecuted; use Core\Mcp\Events\ToolExecuted;
use Core\Mcp\Listeners\RecordToolExecution; use Core\Mcp\Listeners\RecordToolExecution;
@ -18,6 +19,7 @@ use Core\Mcp\Services\ToolDependencyService;
use Core\Mcp\Services\ToolRegistry; use Core\Mcp\Services\ToolRegistry;
use Core\Mcp\Services\ToolVersionService; use Core\Mcp\Services\ToolVersionService;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class Boot extends ServiceProvider class Boot extends ServiceProvider
@ -35,6 +37,7 @@ class Boot extends ServiceProvider
public static array $listens = [ public static array $listens = [
AdminPanelBooting::class => 'onAdminPanel', AdminPanelBooting::class => 'onAdminPanel',
ConsoleBooting::class => 'onConsole', ConsoleBooting::class => 'onConsole',
McpRoutesRegistering::class => 'onMcpRoutes',
McpToolsRegistering::class => 'onMcpTools', McpToolsRegistering::class => 'onMcpTools',
]; ];
@ -87,8 +90,42 @@ class Boot extends ServiceProvider
$event->livewire('mcp.admin.tool-version-manager', View\Modal\Admin\ToolVersionManager::class); $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 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\McpAgentServerCommand::class);
$event->command(Console\Commands\PruneMetricsCommand::class); $event->command(Console\Commands\PruneMetricsCommand::class);
$event->command(Console\Commands\VerifyAuditLogCommand::class); $event->command(Console\Commands\VerifyAuditLogCommand::class);

View file

@ -2,14 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
namespace Mod\Api\Controllers; namespace Core\Mcp\Controllers;
use Core\Front\Controller; use Core\Front\Controller;
use Core\Mcp\Services\McpQuotaService; use Core\Mcp\Services\McpQuotaService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Mod\Api\Models\ApiKey; use Core\Api\Models\ApiKey;
use Core\Mcp\Models\McpApiRequest; use Core\Mcp\Models\McpApiRequest;
use Core\Mcp\Models\McpToolCall; use Core\Mcp\Models\McpToolCall;
use Core\Mcp\Services\McpWebhookDispatcher; use Core\Mcp\Services\McpWebhookDispatcher;
@ -119,11 +119,11 @@ class McpApiController extends Controller
$startTime = microtime(true); $startTime = microtime(true);
try { try {
// Execute the tool via artisan command // Execute the tool via in-process registry or artisan fallback
$result = $this->executeToolViaArtisan( $result = $this->executeTool(
$validated['server'],
$validated['tool'], $validated['tool'],
$validated['arguments'] ?? [] $validated['arguments'] ?? [],
$apiKey
); );
$durationMs = (int) ((microtime(true) - $startTime) * 1000); $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 (! app()->bound($registryClass)) {
if (! $command) { throw new \RuntimeException("AgentToolRegistry not available — is the agentic module installed?");
throw new \RuntimeException("Unknown server: {$server}");
} }
// Build MCP request $registry = app($registryClass);
$mcpRequest = [
'jsonrpc' => '2.0',
'id' => uniqid(),
'method' => 'tools/call',
'params' => [
'name' => $tool,
'arguments' => $arguments,
],
];
// Execute via process if (! $registry->has($tool)) {
$process = proc_open( throw new \RuntimeException("Tool not found: {$tool}");
['php', 'artisan', $command], }
[
0 => ['pipe', 'r'], $context = [];
1 => ['pipe', 'w'],
2 => ['pipe', 'w'], if ($apiKey?->workspace_id) {
], $context['workspace_id'] = $apiKey->workspace_id;
$pipes, }
base_path()
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, bool $success,
?string $error = null ?string $error = null
): void { ): void {
McpToolCall::log( try {
serverId: $request['server'], McpToolCall::log(
toolName: $request['tool'], serverId: $request['server'],
params: $request['arguments'] ?? [], toolName: $request['tool'],
success: $success, params: $request['arguments'] ?? [],
durationMs: $durationMs, success: $success,
errorMessage: $error, durationMs: $durationMs,
workspaceId: $apiKey?->workspace_id 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; namespace Core\Mcp\Middleware;
use Core\Mod\Api\Models\ApiKey; use Core\Api\Models\ApiKey;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;

View file

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

View file

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

View file

@ -1,43 +1,41 @@
<?php <?php
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Mod\Mcp\Middleware\McpAuthenticate; use Core\Mcp\Middleware\McpAuthenticate;
use Website\Mcp\Controllers\McpRegistryController; 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. | Human-readable documentation portal for the MCP domain.
| These routes serve both human-readable docs and machine-readable JSON. | 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(config('mcp.domain'))->name('mcp.')->group(function () {
// Agent discovery endpoint (always JSON, no auth)
Route::domain($mcpDomain)->name('mcp.')->group(function () {
// Agent discovery endpoint (always JSON)
Route::get('.well-known/mcp-servers.json', [McpRegistryController::class, 'registry']) Route::get('.well-known/mcp-servers.json', [McpRegistryController::class, 'registry'])
->name('registry'); ->name('registry');
// Landing page // ── Human-readable portal (optional auth) ────────────────────
Route::get('/', [McpRegistryController::class, 'landing']) Route::get('/', [McpRegistryController::class, 'landing'])
->middleware(McpAuthenticate::class.':optional') ->middleware(McpAuthenticate::class.':optional')
->name('landing'); ->name('landing');
// Server list (HTML/JSON based on Accept header)
Route::get('servers', [McpRegistryController::class, 'index']) Route::get('servers', [McpRegistryController::class, 'index'])
->middleware(McpAuthenticate::class.':optional') ->middleware(McpAuthenticate::class.':optional')
->name('servers.index'); ->name('servers.index');
// Server detail (supports .json extension)
Route::get('servers/{id}', [McpRegistryController::class, 'show']) Route::get('servers/{id}', [McpRegistryController::class, 'show'])
->middleware(McpAuthenticate::class.':optional') ->middleware(McpAuthenticate::class.':optional')
->name('servers.show') ->name('servers.show')
->where('id', '[a-z0-9-]+(?:\.json)?'); ->where('id', '[a-z0-9-]+');
// Connection config page
Route::get('connect', [McpRegistryController::class, 'connect']) Route::get('connect', [McpRegistryController::class, 'connect'])
->middleware(McpAuthenticate::class.':optional') ->middleware(McpAuthenticate::class.':optional')
->name('connect'); ->name('connect');

View file

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

View file

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

View file

@ -149,13 +149,14 @@
<p class="text-zinc-600 dark:text-zinc-400 mb-4 text-sm"> <p class="text-zinc-600 dark:text-zinc-400 mb-4 text-sm">
Call an MCP tool via HTTP POST: Call an MCP tool via HTTP POST:
</p> </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 "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"server": "commerce", "server": "openbrain",
"tool": "product_list", "tool": "brain_recall",
"arguments": {} "arguments": { "query": "recent decisions" }
}'</code></pre> }'</code></pre>
</div> </div>
</div> </div>

View file

@ -1,11 +1,15 @@
<x-layouts.mcp> <x-layouts::mcp>
<x-slot:title>Setup Guide</x-slot:title> <x-slot:title>Setup Guide</x-slot:title>
@php
$mcpUrl = request()->getSchemeAndHttpHost();
@endphp
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white">Setup Guide</h1> <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"> <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> </p>
</div> </div>
@ -28,7 +32,7 @@
</a> </a>
</div> </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="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="flex items-center space-x-3 mb-4">
<div class="p-2 bg-cyan-100 dark:bg-cyan-900/30 rounded-lg"> <div class="p-2 bg-cyan-100 dark:bg-cyan-900/30 rounded-lg">
@ -37,143 +41,118 @@
<div> <div>
<h2 class="text-xl font-semibold text-zinc-900 dark:text-white">HTTP API</h2> <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"> <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> </span>
</div> </div>
</div> </div>
<p class="text-zinc-600 dark:text-zinc-400 mb-4"> <p class="text-zinc-600 dark:text-zinc-400 mb-6">
Call MCP tools from any language or platform using standard HTTP requests. Call MCP tools from any language, platform, or AI agent using standard HTTP requests.
Perfect for external integrations, webhooks, and remote agents. Works with Claude Code, Cursor, custom agents, webhooks, and any HTTP client.
</p> </p>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">1. Get your API key</h3> <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"> <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> </p>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">2. Call a tool</h3> <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 -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-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 "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"server": "commerce", "server": "openbrain",
"tool": "product_list", "tool": "brain_recall",
"arguments": { "category": "hosting" } "arguments": { "query": "authentication decisions" }
}'</code></pre> }'</code></pre>
<h3 class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-2">3. List available tools</h3> <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 https://mcp.host.uk.com/api/v1/mcp/servers \ <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> -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="mt-6 p-4 bg-zinc-50 dark:bg-zinc-900/50 rounded-lg">
<div class="flex items-center justify-between mb-2"> <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"> <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 View OpenAPI Spec
</a> </a>
</div> </div>
<div class="space-y-2 text-sm"> <div class="space-y-2 text-sm">
<div class="flex items-center justify-between"> <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> <span class="text-zinc-500">List all servers</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">GET /api/v1/mcp/servers/{id}</code> <code class="text-zinc-600 dark:text-zinc-400">GET /servers/{id}</code>
<span class="text-zinc-500">Server details</span> <span class="text-zinc-500">Server details + tools</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<code class="text-zinc-600 dark:text-zinc-400">GET /api/v1/mcp/servers/{id}/tools</code> <code class="text-zinc-600 dark:text-zinc-400">POST /tools/call</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>
<span class="text-zinc-500">Execute a tool</span> <span class="text-zinc-500">Execute a tool</span>
</div> </div>
<div class="flex items-center justify-between"> <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> <span class="text-zinc-500">Read a resource</span>
</div> </div>
</div> </div>
</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="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="flex items-center space-x-3 mb-4">
<div class="p-2 bg-violet-100 dark:bg-violet-900/30 rounded-lg"> <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>
<div> <div>
<h2 class="text-xl font-semibold text-zinc-900 dark:text-white">Stdio (Local)</h2> <h2 class="text-xl font-semibold text-zinc-900 dark:text-white">Code Examples</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>
</div> </div>
</div> </div>
<p class="text-zinc-600 dark:text-zinc-400 mb-4"> <div class="space-y-6">
Direct stdio connection for Claude Code and other local AI agents. <!-- Python -->
Ideal for OSS framework users running their own Host Hub instance. <div>
</p> <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"> resp = requests.post(
<summary class="cursor-pointer text-sm font-medium text-cyan-600 dark:text-cyan-400 hover:text-cyan-700"> "{{ $mcpUrl }}/tools/call",
Show stdio configuration headers={"Authorization": "Bearer hk_your_key"},
</summary> json={
"server": "openbrain",
<div class="mt-4 space-y-6"> "tool": "brain_recall",
<!-- Claude Code --> "arguments": {"query": "recent decisions"}
<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"]
} }
} )
}</code></pre> print(resp.json())</code></pre>
</div>
</div> </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> </div>
<!-- Authentication Methods --> <!-- Authentication Methods -->
@ -199,6 +178,28 @@
check your key's server scopes in your admin dashboard. check your key's server scopes in your admin dashboard.
</p> </p>
</div> </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> </div>
<!-- Help --> <!-- Help -->
@ -208,10 +209,10 @@
<flux:button href="{{ route('mcp.servers.index') }}" icon="server-stack"> <flux:button href="{{ route('mcp.servers.index') }}" icon="server-stack">
Browse Servers Browse Servers
</flux:button> </flux:button>
<flux:button href="https://host.uk.com/contact" variant="ghost"> <flux:button href="{{ route('mcp.openapi.json') }}" icon="code-bracket" variant="ghost" target="_blank">
Contact Support OpenAPI Spec
</flux:button> </flux:button>
</div> </div>
</div> </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> <x-slot:title>MCP Servers</x-slot:title>
<div class="mb-8"> <div class="mb-8">
@ -32,9 +32,15 @@
@case('supporthost') @case('supporthost')
<flux:icon.chat-bubble-left-right class="w-6 h-6 text-cyan-600 dark:text-cyan-400" /> <flux:icon.chat-bubble-left-right class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break @break
@case('openbrain')
<flux:icon.light-bulb class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('analyticshost') @case('analyticshost')
<flux:icon.chart-bar class="w-6 h-6 text-cyan-600 dark:text-cyan-400" /> <flux:icon.chart-bar class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break @break
@case('eaas')
<flux:icon.shield-check class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@default @default
<flux:icon.server class="w-6 h-6 text-cyan-600 dark:text-cyan-400" /> <flux:icon.server class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@endswitch @endswitch
@ -123,4 +129,4 @@
</div> </div>
</div> </div>
@endif @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:title>API Keys</x-slot:title>
<x-slot:description>Manage API keys for MCP server access.</x-slot:description> <x-slot:description>Manage API keys for MCP server access.</x-slot:description>
<livewire:mcp.api-key-manager :workspace="$workspace" /> <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: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 --> <!-- Hero -->
<div class="text-center mb-16"> <div class="text-center mb-16">
<h1 class="text-4xl font-bold text-zinc-900 dark:text-white mb-4"> <h1 class="text-4xl font-bold text-zinc-900 dark:text-white mb-4">
Host UK MCP Ecosystem MCP Ecosystem
</h1> </h1>
<p class="text-xl text-zinc-600 dark:text-zinc-400 max-w-2xl mx-auto mb-8"> <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">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">Agent-optimised</span> &bull;
<span class="text-cyan-600 dark:text-cyan-400">Human-friendly</span> <span class="text-cyan-600 dark:text-cyan-400">Human-friendly</span>
@ -93,9 +93,15 @@
@case('supporthost') @case('supporthost')
<flux:icon.chat-bubble-left-right class="w-6 h-6 text-cyan-600 dark:text-cyan-400" /> <flux:icon.chat-bubble-left-right class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break @break
@case('openbrain')
<flux:icon.light-bulb class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@case('analyticshost') @case('analyticshost')
<flux:icon.chart-bar class="w-6 h-6 text-cyan-600 dark:text-cyan-400" /> <flux:icon.chart-bar class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break @break
@case('eaas')
<flux:icon.shield-check class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@break
@default @default
<flux:icon.server class="w-6 h-6 text-cyan-600 dark:text-cyan-400" /> <flux:icon.server class="w-6 h-6 text-cyan-600 dark:text-cyan-400" />
@endswitch @endswitch
@ -180,18 +186,21 @@
@endif @endif
<!-- Quick Start --> <!-- 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"> <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> <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"> <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> </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 "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"server": "commerce", "server": "openbrain",
"tool": "product_list", "tool": "brain_recall",
"arguments": {} "arguments": { "query": "recent decisions" }
}'</code></pre> }'</code></pre>
<div class="flex flex-wrap items-center gap-4"> <div class="flex flex-wrap items-center gap-4">
<flux:button href="{{ route('mcp.connect') }}" icon="document-text" variant="primary"> <flux:button href="{{ route('mcp.connect') }}" icon="document-text" variant="primary">
@ -202,4 +211,4 @@
</flux:button> </flux:button>
</div> </div>
</section> </section>
</x-layouts.mcp> </x-layouts::mcp>

View file

@ -236,7 +236,7 @@
<div class="space-y-3 text-sm"> <div class="space-y-3 text-sm">
<div> <div>
<span class="text-zinc-500 dark:text-zinc-400">Endpoint:</span> <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>
<div> <div>
<span class="text-zinc-500 dark:text-zinc-400">Method:</span> <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:title>{{ $server['name'] }}</x-slot:title>
<x-slot:description>{{ $server['tagline'] ?? $server['description'] ?? '' }}</x-slot:description> <x-slot:description>{{ $server['tagline'] ?? $server['description'] ?? '' }}</x-slot:description>
@ -29,9 +29,15 @@
@case('supporthost') @case('supporthost')
<flux:icon.chat-bubble-left-right class="w-8 h-8 text-cyan-600 dark:text-cyan-400" /> <flux:icon.chat-bubble-left-right class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break @break
@case('openbrain')
<flux:icon.light-bulb class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break
@case('analyticshost') @case('analyticshost')
<flux:icon.chart-bar class="w-8 h-8 text-cyan-600 dark:text-cyan-400" /> <flux:icon.chart-bar class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break @break
@case('eaas')
<flux:icon.shield-check class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break
@case('upstream') @case('upstream')
<flux:icon.arrows-up-down class="w-8 h-8 text-cyan-600 dark:text-cyan-400" /> <flux:icon.arrows-up-down class="w-8 h-8 text-cyan-600 dark:text-cyan-400" />
@break @break
@ -96,18 +102,28 @@
</div> </div>
<!-- Connection --> <!-- Connection -->
@if(!empty($server['connection'])) @php
<div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 p-6 mb-8"> $mcpUrl = request()->getSchemeAndHttpHost();
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white mb-4">Connection</h2> @endphp
<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">{ <div class="bg-white dark:bg-zinc-800 rounded-xl border border-zinc-200 dark:border-zinc-700 p-6 mb-8">
"{{ $server['id'] }}": { <h2 class="text-lg font-semibold text-zinc-900 dark:text-white mb-4">Connection</h2>
"command": "{{ $server['connection']['command'] ?? 'php' }}", <p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
"args": {!! json_encode($server['connection']['args'] ?? ['artisan', 'mcp:agent-server']) !!}, Call tools on this server via HTTP:
"cwd": "{{ $server['connection']['cwd'] ?? '/path/to/project' }}" </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 \
}</code></pre> -H "Authorization: Bearer YOUR_API_KEY" \
</div> -H "Content-Type: application/json" \
@endif -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 --> <!-- Tools -->
@if(!empty($server['tools'])) @if(!empty($server['tools']))
@ -224,4 +240,4 @@
View Usage Analytics View Usage Analytics
</a> </a>
</div> </div>
</x-layouts.mcp> </x-layouts::mcp>

View file

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

View file

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

View file

@ -18,7 +18,7 @@ use Symfony\Component\Yaml\Yaml;
* A browser-based UI for testing MCP tool calls. * A browser-based UI for testing MCP tool calls.
* Allows users to select a server, pick a tool, and execute it with custom parameters. * 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 class McpPlayground extends Component
{ {
public string $selectedServer = ''; public string $selectedServer = '';

View file

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

View file

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

View file

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