Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions app/Http/Controllers/McpController.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ public function pageApi(string $platform, string $version, string $path): JsonRe
return response()->json(['page' => $page]);
}

public function apisApi(string $platform, string $version): JsonResponse
public function edgeComponentsApi(string $platform, string $version): JsonResponse
{
$apis = $this->docsSearch->listApis($platform, $version);
$components = $this->docsSearch->listEdgeComponents($platform, $version);

return response()->json(['apis' => $apis]);
return response()->json(['edge_components' => $components]);
}

public function navigationApi(string $platform, string $version): JsonResponse
Expand Down Expand Up @@ -182,22 +182,22 @@ protected function getToolDefinitions(): array
],
],
[
'name' => 'list_apis',
'description' => 'List all native APIs for a platform/version',
'name' => 'list_edge_components',
'description' => 'List EDGE / SuperNative UI components documented for a platform/version so agents build the NativePHP way (native UI via Blade EDGE components). Defaults to the latest version for the platform when version is omitted.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'platform' => [
'type' => 'string',
'enum' => ['desktop', 'mobile'],
'description' => 'Platform to list APIs for',
'description' => 'Platform to list EDGE components for (default: mobile)',
'default' => 'mobile',
],
'version' => [
'type' => 'string',
'description' => 'Version number',
'description' => 'Version number (optional; defaults to the latest for the platform)',
],
],
'required' => ['platform', 'version'],
],
],
[
Expand Down Expand Up @@ -285,7 +285,7 @@ protected function handleToolCall(string $name, array $args): array
return match ($name) {
'search_docs' => $this->toolSearchDocs($args),
'get_page' => $this->toolGetPage($args),
'list_apis' => $this->toolListApis($args),
'list_edge_components' => $this->toolListEdgeComponents($args),
'get_navigation' => $this->toolGetNavigation($args),
'search_plugins' => $this->toolSearchPlugins($args),
'get_plugin' => $this->toolGetPlugin($args),
Expand Down Expand Up @@ -350,27 +350,29 @@ protected function toolGetPage(array $args): array
];
}

protected function toolListApis(array $args): array
protected function toolListEdgeComponents(array $args): array
{
$platform = $args['platform'] ?? '';
$version = $args['version'] ?? '';
$platform = $args['platform'] ?? 'mobile';
$latestVersions = $this->docsSearch->getLatestVersions();
$version = $args['version'] ?? ($latestVersions[$platform] ?? '');

$apis = $this->docsSearch->listApis($platform, $version);
$components = $this->docsSearch->listEdgeComponents($platform, $version);

if (empty($apis)) {
if (empty($components)) {
return [
'content' => [['type' => 'text', 'text' => "No APIs found for {$platform} v{$version}"]],
'content' => [['type' => 'text', 'text' => "No EDGE components found for {$platform} v{$version}"]],
];
}

$formatted = collect($apis)->map(function ($api) {
$desc = $api['description'] ?: 'No description';
$formatted = collect($components)->map(function ($component) {
$desc = $component['description'] ?: 'No description';
$path = $component['id'];

return "- **{$api['title']}** ({$api['slug']})\n {$desc}";
return "- **{$component['title']}** ({$component['slug']})\n Path: {$path}\n {$desc}";
})->join("\n");

return [
'content' => [['type' => 'text', 'text' => "# {$platform} v{$version} APIs\n\n{$formatted}"]],
'content' => [['type' => 'text', 'text' => "# {$platform} v{$version} EDGE components\n\n{$formatted}"]],
];
}

Expand Down
5 changes: 3 additions & 2 deletions app/Services/DocsSearchService.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,15 @@ public function getPageByPath(string $path): ?array
return $this->getPage($platform, $version, implode('/', $parts), $slug);
}

public function listApis(string $platform, string $version): array
public function listEdgeComponents(string $platform, string $version): array
{
if (! $this->sanitizePlatform($platform) || ! $this->sanitizeVersion($version)) {
return [];
}

return collect($this->getAllPages($platform, $version))
->filter(fn ($page) => $page['section'] === 'apis')
->filter(fn ($page) => $page['section'] === 'edge-components'
|| str_starts_with($page['section'], 'edge-components/'))
->sortBy('order')
->values()
->toArray();
Expand Down
67 changes: 67 additions & 0 deletions resources/views/docs/mobile/4/getting-started/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: MCP Docs Server
order: 275
---

The full NativePHP documentation — for both [Mobile](/docs/mobile/getting-started/introduction) and [Desktop](/docs/desktop/getting-started/introduction) — is available over [MCP](https://modelcontextprotocol.io). Agents such as Claude Code, Cursor, and Copilot can search and read the docs while they work, instead of relying on training-data memory alone.

It's hosted by NativePHP. There's nothing to install and no API key to create — just point your agent at this URL:

```
https://nativephp.com/api/mcp/message
```

## Quick connect

### Claude Code

```shell
claude mcp add --transport http nativephp-docs https://nativephp.com/api/mcp/message
```

Or commit `.mcp.json` to your repo so the whole team picks it up:

```json
{
"mcpServers": {
"nativephp-docs": {
"type": "http",
"url": "https://nativephp.com/api/mcp/message"
}
}
}
```

### Cursor

Create `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` to enable it everywhere:

```json
{
"mcpServers": {
"nativephp-docs": {
"type": "http",
"url": "https://nativephp.com/api/mcp/message"
}
}
}
```

## What your agent can do

Once connected, your agent gets these tools:

- **`search_docs`** — full-text search across every platform and version (use this for current Mobile v4 docs)
- **`get_page`** — fetch a full page by path (e.g. `mobile/4/plugins/core/camera` or `mobile/4/edge-components/button`)
- **`get_navigation`** — the sidebar for a platform and version
- **`list_edge_components`** — list EDGE / SuperNative UI components for a platform (defaults to latest mobile) so agents build native UI via Blade EDGE components
- **`search_plugins`** — search the public plugin marketplace (where Mobile v3+ native APIs live)
- **`get_plugin`** — fetch one marketplace plugin by composer name

## Pair it with Laravel Boost

This MCP tells your agent what NativePHP _can_ do. [Laravel Boost](https://laravel.com/ai/boost) tells it about _your_ application — routes, models, config, and package versions. Running both together works better than either alone.

## More clients and details

For VS Code / Copilot, `mcp-remote`, REST mirrors, rate limits, and the full client matrix, see the [Docs MCP Server](/mcp) page.
15 changes: 9 additions & 6 deletions resources/views/mcp-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,15 @@ Returns the whole sidebar for a `platform` and `version`, grouped by section and
in the order you see it on the site. Useful when an agent wants to orient itself
before searching, or to check whether a topic is documented at all.

### `list_apis`
### `list_edge_components`

Lists the pages in a version's `apis` section. That section only exists in the
Mobile v1 and v2 docs — from v3 onwards the native APIs are documented under
Plugins, and the Desktop docs have no `apis` section at all. For anything
current, use `get_navigation` or `search_docs` instead.
Lists the EDGE / SuperNative UI components documented for a `platform` and
optional `version` — the Blade components agents should use to build native
UI the NativePHP way. Defaults to `mobile` and the latest published version for
that platform when those args are omitted. Each result includes a path you can
hand straight to `get_page` (for example `mobile/4/edge-components/button`).
Mobile v2+ ships an `edge-components` section; Desktop currently has none, so
the list is empty there.

### `search_plugins`

Expand Down Expand Up @@ -161,7 +164,7 @@ MCP client:
- `/api/mcp/search?q=camera&platform=mobile` — search results as JSON
- `/api/mcp/page/{platform}/{version}/{section}/{slug}` — a single page
- `/api/mcp/navigation/{platform}/{version}` — the docs navigation tree
- `/api/mcp/apis/{platform}/{version}` — the `apis` section listing
- `/api/mcp/edge-components/{platform}/{version}` — EDGE / SuperNative component listing
- `/api/mcp/plugins?q=camera&type=free&limit=10` — marketplace plugin search
- `/api/mcp/plugins/{vendor}/{package}` — one marketplace plugin
- `/api/mcp/health` — liveness check, and the versions currently published
Expand Down
2 changes: 1 addition & 1 deletion routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
Route::get('page/{platform}/{version}/{path}', [McpController::class, 'pageApi'])
->where('path', '.*')
->name('mcp.api.page');
Route::get('apis/{platform}/{version}', [McpController::class, 'apisApi'])->name('mcp.api.apis');
Route::get('edge-components/{platform}/{version}', [McpController::class, 'edgeComponentsApi'])->name('mcp.api.edge-components');
Route::get('navigation/{platform}/{version}', [McpController::class, 'navigationApi'])->name('mcp.api.navigation');

Route::get('plugins', [McpController::class, 'pluginsSearchApi'])->name('mcp.api.plugins.search');
Expand Down
109 changes: 108 additions & 1 deletion tests/Feature/DocsMcpServerPageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function the_documented_message_endpoint_lists_the_documented_tools(): vo
$tools = collect($response->json('result.tools'))->pluck('name')->all();

$this->assertEqualsCanonicalizing(
['search_docs', 'get_page', 'list_apis', 'get_navigation', 'search_plugins', 'get_plugin'],
['search_docs', 'get_page', 'list_edge_components', 'get_navigation', 'search_plugins', 'get_plugin'],
$tools,
);
}
Expand Down Expand Up @@ -137,4 +137,111 @@ public function the_docs_no_longer_carry_a_duplicate_copy_of_this_page(): void
'The MCP server is documented once, at '.route('mcp').'.',
);
}

#[Test]
public function mobile_v4_getting_started_documents_the_public_mcp_endpoint(): void
{
$this->withoutVite()
->get('/docs/mobile/4/getting-started/mcp')
->assertOk()
->assertSee('MCP Docs Server')
->assertSee('https://nativephp.com/api/mcp/message');
}

#[Test]
public function list_edge_components_returns_mobile_v4_components(): void
{
$response = $this->postJson('/api/mcp/message', [
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'tools/call',
'params' => [
'name' => 'list_edge_components',
'arguments' => ['platform' => 'mobile', 'version' => '4'],
],
]);

$response->assertOk();

$text = $response->json('result.content.0.text');

$this->assertStringContainsString('mobile v4 EDGE components', $text);
$this->assertStringContainsString('button', $text);
$this->assertStringContainsString('text', $text);
$this->assertStringContainsString('Path: mobile/4/edge-components/button', $text);
$this->assertStringNotContainsString('Unknown tool', $text);
}

#[Test]
public function list_edge_components_defaults_version_to_latest_mobile(): void
{
$latest = app(DocsSearchService::class)->getLatestVersions()['mobile'];

$response = $this->postJson('/api/mcp/message', [
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'tools/call',
'params' => [
'name' => 'list_edge_components',
'arguments' => ['platform' => 'mobile'],
],
]);

$response->assertOk();

$text = $response->json('result.content.0.text');

$this->assertStringContainsString("mobile v{$latest} EDGE components", $text);
$this->assertStringContainsString("Path: mobile/{$latest}/edge-components/", $text);
}

#[Test]
public function list_apis_is_no_longer_registered(): void
{
$list = $this->postJson('/api/mcp/message', [
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'tools/list',
]);

$tools = collect($list->json('result.tools'))->pluck('name')->all();
$this->assertNotContains('list_apis', $tools);

$call = $this->postJson('/api/mcp/message', [
'jsonrpc' => '2.0',
'id' => 2,
'method' => 'tools/call',
'params' => [
'name' => 'list_apis',
'arguments' => ['platform' => 'mobile', 'version' => '2'],
],
]);

$this->assertTrue($call->json('result.isError'));
$this->assertStringContainsString('Unknown tool: list_apis', $call->json('result.content.0.text'));
}

#[Test]
public function the_edge_components_rest_endpoint_lists_mobile_v4_components(): void
{
$response = $this->getJson('/api/mcp/edge-components/mobile/4');

$response->assertOk()
->assertJsonStructure(['edge_components' => [['title', 'slug', 'description', 'section', 'id']]]);

$slugs = collect($response->json('edge_components'))->pluck('slug');

$this->assertTrue($slugs->contains('button'));
$this->assertTrue($slugs->contains('text'));
$this->assertTrue(
collect($response->json('edge_components'))->every(fn ($c) => $c['section'] === 'edge-components'
|| str_starts_with($c['section'], 'edge-components/')),
);
}

#[Test]
public function the_legacy_apis_rest_endpoint_is_gone(): void
{
$this->getJson('/api/mcp/apis/mobile/2')->assertNotFound();
}
}
6 changes: 3 additions & 3 deletions tests/Feature/McpSecurityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ public function test_page_api_rejects_an_absolute_looking_section(): void
$response->assertStatus(404);
}

public function test_apis_endpoint_rejects_invalid_platform(): void
public function test_edge_components_endpoint_rejects_invalid_platform(): void
{
$response = $this->getJson('/api/mcp/apis/../1');
$response = $this->getJson('/api/mcp/edge-components/../1');

$response->assertStatus(200);
$response->assertJson(['apis' => []]);
$response->assertJson(['edge_components' => []]);
}

public function test_navigation_endpoint_rejects_invalid_version(): void
Expand Down