mcp/docs/development.md

373 lines
10 KiB
Markdown
Raw Permalink Normal View History

---
title: Development
description: How to build, test, and contribute to the core/mcp repository.
---
# Development
## Prerequisites
- **Go 1.26+** -- the module uses Go 1.26 features (range-over-func
iterators, `reflect.Type.Fields()`)
- **PHP 8.2+** -- required by the Laravel package
- **Composer** -- for PHP dependency management
- **Core CLI** -- `core build`, `core go test`, etc. (built from
`forge.lthn.ai/core/cli`)
- **Go workspace** -- this module is part of the workspace at `~/Code/go.work`
## Building
### Go binary
```bash
# From the repo root
core build # produces ./core-mcp (arm64 by default)
# Or with Go directly
go build -o core-mcp ./cmd/core-mcp/
```
Build configuration lives in `.core/build.yaml`:
```yaml
project:
name: core-mcp
binary: core-mcp
```
### PHP package
The PHP code is consumed as a Composer package. There is no standalone build
step. To develop locally, symlink or use a Composer path repository in your
Laravel application:
```json
{
"repositories": [
{
"type": "path",
"url": "../core/mcp"
}
]
}
```
Then run `composer require lthn/mcp:@dev`.
## Testing
### Go tests
```bash
# Run all tests
core go test
# Run a single test
core go test --run TestBridgeToAPI
# With coverage
core go cov
core go cov --open # opens HTML report in browser
# Full QA (format + vet + lint + test)
core go qa
core go qa full # also runs race detector, vuln scan, security audit
```
Test files follow the `_Good`, `_Bad`, `_Ugly` suffix convention:
| Suffix | Meaning |
|--------|---------|
| `_Good` | Happy path -- expected behaviour with valid inputs |
| `_Bad` | Error paths -- expected failures with invalid inputs |
| `_Ugly` | Edge cases -- panics, nil pointers, concurrent access |
Key test files:
| File | What it covers |
|------|----------------|
| `mcp_test.go` | Service creation, workspace sandboxing, file operations |
| `registry_test.go` | Tool recording, schema extraction, REST handler creation |
| `bridge_test.go` | `BridgeToAPI`, JSON error classification, 10 MB body limit |
| `subsystem_test.go` | Subsystem registration and shutdown |
| `transport_tcp_test.go` | TCP transport, loopback default, `0.0.0.0` warning |
| `transport_e2e_test.go` | End-to-end TCP client/server round-trip |
| `tools_metrics_test.go` | Duration parsing, metrics record/query |
| `tools_ml_test.go` | ML subsystem tool registration |
| `tools_process_test.go` | Process start/stop/kill/list/output/input |
| `tools_process_ci_test.go` | CI-safe process tests (no external binaries) |
| `tools_rag_test.go` | RAG query/ingest/collections |
| `tools_rag_ci_test.go` | CI-safe RAG tests (no Qdrant required) |
| `tools_webview_test.go` | Webview tool registration and error handling |
| `tools_ws_test.go` | WebSocket start/info tools |
| `iter_test.go` | Iterator helpers (`SubsystemsSeq`, `ToolsSeq`) |
| `integration_test.go` | Cross-subsystem integration |
| `ide/bridge_test.go` | IDE bridge connection, message dispatch |
| `ide/tools_test.go` | IDE tool registration |
| `brain/brain_test.go` | Brain subsystem registration and bridge-nil handling |
### PHP tests
```bash
# From the repo root (or src/php/)
composer test
# Single test
composer test -- --filter=SqlQueryValidatorTest
```
PHP tests use Pest syntax. Key test files:
| File | What it covers |
|------|----------------|
| `SqlQueryValidatorTest.php` | Blocked keywords, injection patterns, whitelist |
| `McpQuotaServiceTest.php` | Quota recording and enforcement |
| `QueryAuditServiceTest.php` | Audit log recording |
| `QueryExecutionServiceTest.php` | Query execution with limits and timeouts |
| `ToolAnalyticsServiceTest.php` | Analytics aggregation |
| `ToolDependencyServiceTest.php` | Dependency validation |
| `ToolVersionServiceTest.php` | Version management |
| `ValidateWorkspaceContextMiddlewareTest.php` | Workspace context validation |
| `WorkspaceContextSecurityTest.php` | Multi-tenant isolation |
## Code style
### Go
- Format with `core go fmt` (uses `gofmt`)
- Lint with `core go lint` (uses `golangci-lint`)
- Vet with `core go vet`
- All three run automatically via `core go qa`
### PHP
- Format with `composer lint` (uses Laravel Pint, PSR-12)
- Format only changed files: `./vendor/bin/pint --dirty`
### General conventions
- **UK English** in all user-facing strings and documentation (colour,
organisation, centre, normalise, serialise).
- **Strict types** in every PHP file: `declare(strict_types=1);`
- **SPDX headers** in Go files: `// SPDX-License-Identifier: EUPL-1.2`
- **Type hints** on all PHP parameters and return types.
- Conventional commits: `type(scope): description`
## Project structure
```
core/mcp/
+-- .core/
| +-- build.yaml # Build configuration
+-- cmd/
| +-- core-mcp/
| | +-- main.go # Binary entry point
| +-- mcpcmd/
| | +-- cmd_mcp.go # CLI command registration
| +-- brain-seed/
| +-- main.go # OpenBrain import utility
+-- pkg/
| +-- mcp/
| +-- mcp.go # Service, file tools, Run()
| +-- registry.go # ToolRecord, addToolRecorded, schema extraction
| +-- subsystem.go # Subsystem interface, WithSubsystem option
| +-- bridge.go # BridgeToAPI (MCP-to-REST adapter)
| +-- transport_stdio.go
| +-- transport_tcp.go
| +-- transport_unix.go
| +-- tools_metrics.go # Metrics record/query
| +-- tools_ml.go # MLSubsystem (generate, score, probe, status, backends)
| +-- tools_process.go # Process management tools
| +-- tools_rag.go # RAG query/ingest/collections
| +-- tools_webview.go # Chrome DevTools automation
| +-- tools_ws.go # WebSocket server tools
| +-- brain/
| | +-- brain.go # Brain subsystem
| | +-- tools.go # remember/recall/forget/list tools
| +-- ide/
| +-- ide.go # IDE subsystem
| +-- config.go # Config, options, defaults
| +-- bridge.go # Laravel WebSocket bridge
| +-- tools_chat.go
| +-- tools_build.go
| +-- tools_dashboard.go
+-- src/
| +-- php/
| +-- src/
| | +-- Front/Mcp/ # Frontage (middleware group, contracts)
| | +-- Mcp/ # Module (services, models, tools, admin)
| | +-- Website/Mcp/ # Public pages (playground, explorer)
| +-- tests/
| +-- config/
| +-- routes/
+-- composer.json
+-- go.mod
+-- go.sum
```
## Running locally
### MCP server (stdio, for Claude Code)
Add to your Claude Code MCP configuration:
```json
{
"mcpServers": {
"core": {
"command": "/path/to/core-mcp",
"args": ["mcp", "serve", "--workspace", "/path/to/project"]
}
}
}
```
### MCP server (TCP, for multi-client)
```bash
MCP_ADDR=127.0.0.1:9100 ./core-mcp mcp serve
```
Connect with any JSON-RPC client over TCP. Each line is a complete JSON-RPC
message. Maximum message size is 10 MB.
### PHP development server
Use Laravel Valet or the built-in server:
```bash
cd /path/to/laravel-app
php artisan serve
```
The MCP API is available at the configured domain under the routes registered
by `Core\Mcp\Boot::onMcpRoutes`.
### Brain-seed
```bash
# Preview what would be imported
go run ./cmd/brain-seed -dry-run
# Import with API key
go run ./cmd/brain-seed \
-api-key YOUR_KEY \
-api https://lthn.sh/api/v1/mcp \
-plans \
-claude-md
```
## Adding a new Go tool
1. Define input and output structs with `json` tags:
```go
type MyToolInput struct {
Query string `json:"query"`
Limit int `json:"limit,omitempty"`
}
type MyToolOutput struct {
Results []string `json:"results"`
Total int `json:"total"`
}
```
2. Write the handler function:
```go
func (s *Service) myTool(
ctx context.Context,
req *mcp.CallToolRequest,
input MyToolInput,
) (*mcp.CallToolResult, MyToolOutput, error) {
// Implementation here
return nil, MyToolOutput{Results: results, Total: len(results)}, nil
}
```
3. Register in `registerTools()`:
```go
addToolRecorded(s, server, "mygroup", &mcp.Tool{
Name: "my_tool",
Description: "Does something useful",
}, s.myTool)
```
The `addToolRecorded` generic function automatically generates JSON Schemas
from the struct tags and creates a REST-compatible handler. No additional
wiring is needed.
## Adding a new Go subsystem
1. Create a new package under `pkg/mcp/`:
```go
package mysubsystem
type Subsystem struct{}
func (s *Subsystem) Name() string { return "mysubsystem" }
func (s *Subsystem) RegisterTools(server *mcp.Server) {
mcp.AddTool(server, &mcp.Tool{
Name: "my_subsystem_tool",
Description: "...",
}, s.handler)
}
```
2. Register when creating the service:
```go
mcp.New(mcp.WithSubsystem(&mysubsystem.Subsystem{}))
```
## Adding a new PHP tool
1. Create a tool class implementing `McpToolHandler`:
```php
namespace Core\Mcp\Tools;
use Core\Front\Mcp\Contracts\McpToolHandler;
use Core\Front\Mcp\McpContext;
class MyTool implements McpToolHandler
{
public static function schema(): array
{
return [
'name' => 'my_tool',
'description' => 'Does something useful',
'inputSchema' => [
'type' => 'object',
'properties' => [
'query' => ['type' => 'string'],
],
'required' => ['query'],
],
];
}
public function handle(array $args, McpContext $context): array
{
return ['result' => 'done'];
}
}
```
2. Register via the `McpToolsRegistering` lifecycle event in your module's
Boot class.
## Contributing
- All changes must pass `core go qa` (Go) and `composer test` (PHP) before
committing.
- Use conventional commits: `feat(mcp): add new tool`, `fix(mcp): handle nil
input`, `docs(mcp): update architecture`.
- Include `Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>` when
pair-programming with Claude.
- Licence: EUPL-1.2. All new files must include the appropriate SPDX header.