Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f75d69444e | ||
| bcbadf3830 | |||
| ef1debf7a9 | |||
|
|
217e9bbfb6 | ||
|
|
dfd712845d | ||
|
|
2458f87c8d |
9 changed files with 497 additions and 129 deletions
13
.forgejo/workflows/ci.yml
Normal file
13
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
uses: core/php/.forgejo/workflows/php-test.yml@main
|
||||
with:
|
||||
coverage: true
|
||||
236
FINDINGS.md
Normal file
236
FINDINGS.md
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# Phase 0 Findings: Environment Assessment
|
||||
|
||||
**Date:** 2026-02-21
|
||||
**Branch:** feat/phase-0-assessment
|
||||
**Issue:** #1
|
||||
|
||||
---
|
||||
|
||||
## 1. Environment Assessment
|
||||
|
||||
### Composer Install
|
||||
|
||||
**Result:** FAILED
|
||||
|
||||
```
|
||||
Your requirements could not be resolved to an installable set of packages.
|
||||
Problem 1
|
||||
- Root composer.json requires host-uk/core, it could not be found in any version
|
||||
```
|
||||
|
||||
`host-uk/core` is a private proprietary package (the Core PHP framework). It is not
|
||||
published to Packagist or a configured private registry accessible in this environment.
|
||||
|
||||
**Impact:** Tests, lint, and static analysis cannot be executed without the vendor
|
||||
directory. All tooling assessment below is based on static code review.
|
||||
|
||||
---
|
||||
|
||||
## 2. Test Baseline
|
||||
|
||||
**Status:** Unable to run (no vendor directory)
|
||||
|
||||
**Configured test runner:** Pest (`vendor/bin/pest`)
|
||||
|
||||
**Test file inventory:**
|
||||
|
||||
| File | Suite | Status |
|
||||
|------|-------|--------|
|
||||
| `tests/Unit/SqlQueryValidatorTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/WorkspaceContextSecurityTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/ToolAnalyticsServiceTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/McpQuotaServiceTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/QueryAuditServiceTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/QueryExecutionServiceTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/ToolDependencyServiceTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/ToolVersionServiceTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/Unit/ValidateWorkspaceContextMiddlewareTest.php` | Unit | Present |
|
||||
| `src/Mcp/Tests/UseCase/ApiKeyManagerBasic.php` | UseCase | Present |
|
||||
|
||||
**Notable gaps:**
|
||||
- No test for `QueryDatabase` tool (the primary entry point)
|
||||
- No test for `ToolRegistry` / `AgentToolRegistry` service
|
||||
- No test for `McpAgentServerCommand` (stdio MCP server)
|
||||
- No test for `AuditLogService` (tamper-evidence verification)
|
||||
- No test for `CircuitBreaker` service
|
||||
- No integration tests at all (`tests/Feature/` is empty)
|
||||
|
||||
---
|
||||
|
||||
## 3. Lint Baseline
|
||||
|
||||
**Tool:** `vendor/bin/pint`
|
||||
**Status:** Unable to run (no vendor directory)
|
||||
|
||||
**Static observation:** All reviewed files contain `declare(strict_types=1)` at the top
|
||||
and follow PSR-12 conventions. Consistent UK English spelling observed throughout
|
||||
(colour, organisation, licence, sanitise, normalise).
|
||||
|
||||
---
|
||||
|
||||
## 4. Static Analysis Baseline
|
||||
|
||||
**Tool:** `vendor/bin/phpstan` (level unknown — not declared in composer.json)
|
||||
**Status:** Unable to run (no vendor directory)
|
||||
|
||||
**Observations from code review:**
|
||||
|
||||
### Type Safety
|
||||
- All public methods have complete parameter and return type hints
|
||||
- Private methods are consistently typed
|
||||
- `SqlQueryValidator::$whitelist` is `array` — could be `array<int, string>` for PHPStan level 5+
|
||||
- `Boot::$listens` uses `array<class-string, string>` — correct
|
||||
|
||||
### Potential PHPStan Issues (estimated level 5)
|
||||
1. `QueryDatabase::getWorkspaceId()` calls `workspace()` global helper — not declared in stubs
|
||||
2. `QueryDatabase::getUserId()` calls `auth()->id()` — return type is `int|string|null`, cast to int without null check
|
||||
3. `QueryDatabase::interpretExplain()` accesses `$rowArray['type']` on `array<string, mixed>` — likely needs type narrowing
|
||||
4. `QueryDatabase::handleExplain()` passes `$explainResults` (array of stdClass) to `interpretExplain()` typed as `array` — needs `array<int, object>`
|
||||
5. `Boot::onMcpTools()` has empty body — PHPStan will warn about unused parameter
|
||||
|
||||
---
|
||||
|
||||
## 5. Architecture Review
|
||||
|
||||
### Package Structure
|
||||
|
||||
```
|
||||
src/Mcp/ # Core\Mcp namespace (103 PHP files)
|
||||
├── Boot.php # ServiceProvider + event-driven registration
|
||||
├── Console/ # 5 Artisan commands
|
||||
├── Context/ # WorkspaceContext value object
|
||||
├── Controllers/ # McpApiController (REST)
|
||||
├── Dependencies/ # Tool dependency system (interface + DTO + enum)
|
||||
├── DTO/ # ToolStats data transfer object
|
||||
├── Events/ # ToolExecuted domain event
|
||||
├── Exceptions/ # 6 custom exceptions (typed hierarchy)
|
||||
├── Lang/en_GB/ # UK English translations
|
||||
├── Listeners/ # RecordToolExecution event listener
|
||||
├── Middleware/ # 5 middleware (auth, quota, workspace context, deps)
|
||||
├── Migrations/ # 5 database migrations
|
||||
├── Models/ # 8 Eloquent models
|
||||
├── Resources/ # AppConfig, ContentResource, DatabaseSchema
|
||||
├── Routes/ # admin.php route file
|
||||
├── Services/ # 18 business logic services
|
||||
├── Tests/ # Unit tests co-located with package
|
||||
├── Tools/ # 10 MCP tool implementations
|
||||
└── View/ # 12 Blade templates + 9 Livewire components
|
||||
|
||||
src/Website/Mcp/ # Core\Website\Mcp namespace
|
||||
└── ... # Web-facing UI module
|
||||
```
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
**1. Event-Driven Module Registration**
|
||||
`Boot.php` uses a `$listens` static array to subscribe to framework lifecycle events
|
||||
(`AdminPanelBooting`, `ConsoleBooting`, `McpToolsRegistering`). This enables lazy-loading
|
||||
of admin UI, commands, and tool registrations without booting the full framework.
|
||||
|
||||
**2. Tool Contract**
|
||||
Tools extend `Laravel\Mcp\Server\Tool` and implement:
|
||||
- `$description` property
|
||||
- `schema(JsonSchema $schema): array` — declares MCP input schema
|
||||
- `handle(Request $request): Response` — executes tool logic
|
||||
|
||||
**3. Defence-in-Depth SQL Validation** (`SqlQueryValidator`)
|
||||
Four sequential layers:
|
||||
1. Dangerous pattern check on raw query (before comment stripping)
|
||||
2. Comment stripping (removes `--`, `#`, `/* */`, `/*!` obfuscation)
|
||||
3. Blocked keyword check (write/admin/export operations)
|
||||
4. Whitelist regex matching (only known-safe SELECT structures pass)
|
||||
|
||||
**4. Workspace Tenant Isolation**
|
||||
`RequiresWorkspaceContext` trait + `ValidateWorkspaceContext` middleware enforce per-request
|
||||
tenant scoping. `MissingWorkspaceContextException` is thrown for unauthenticated context.
|
||||
|
||||
**5. Tier-Based Resource Limits**
|
||||
`McpQuotaService` and `QueryExecutionService` apply different limits per subscription tier:
|
||||
- free / starter / pro / business / enterprise / unlimited
|
||||
- Limits cover: row count, query timeout, daily request quota
|
||||
|
||||
**6. Singleton Service Container**
|
||||
All 8 core services registered as singletons in `Boot::register()`. Each is independently
|
||||
testable and injected via Laravel's container.
|
||||
|
||||
### Notable Issues
|
||||
|
||||
**Issue A — `onMcpTools` is a stub**
|
||||
`Boot::onMcpTools()` contains only a comment:
|
||||
```php
|
||||
public function onMcpTools(McpToolsRegistering $event): void
|
||||
{
|
||||
// MCP tool handlers will be registered here once extracted
|
||||
// from the monolithic McpAgentServerCommand
|
||||
}
|
||||
```
|
||||
This means MCP tools are registered inside `McpAgentServerCommand` rather than being
|
||||
injected via the service container. Refactoring this is P1 work.
|
||||
|
||||
**Issue B — `McpAgentServerCommand` is monolithic**
|
||||
The stdio MCP server command handles tool registration, JSON-RPC dispatch, and tool
|
||||
execution in a single command class. This makes it untestable in isolation.
|
||||
|
||||
**Issue C — `ListTables` tool exists but Schema Exploration is listed as TODO**
|
||||
`src/Mcp/Tools/ListTables.php` exists but the TODO.md item "Schema Exploration Tools"
|
||||
lists adding `ListTables` as pending. This is already implemented.
|
||||
|
||||
**Issue D — No `composer.lock`**
|
||||
No `composer.lock` file is present. Dependency versions are not pinned, which creates
|
||||
reproducibility risk in CI/CD.
|
||||
|
||||
**Issue E — `phpunit.xml` references `vendor/phpunit/phpunit`**
|
||||
The test runner is configured for PHPUnit XML format but Pest is the stated test runner.
|
||||
This is compatible (Pest uses PHPUnit under the hood) but the XML namespace warning will
|
||||
appear until `composer.lock` is generated.
|
||||
|
||||
**Issue F — `tests/Feature/` is empty**
|
||||
No feature/integration tests exist. All tests are unit tests that mock the database.
|
||||
End-to-end request-response flows have no test coverage.
|
||||
|
||||
---
|
||||
|
||||
## 6. Security Observations
|
||||
|
||||
| Finding | Severity | Status |
|
||||
|---------|----------|--------|
|
||||
| SQL injection prevention (multi-layer) | GOOD | Implemented |
|
||||
| Read-only connection enforcement | GOOD | Implemented |
|
||||
| Workspace tenant isolation | GOOD | Implemented |
|
||||
| Audit trail with HMAC verification | GOOD | Implemented |
|
||||
| Tier-based resource limits | GOOD | Implemented |
|
||||
| Circuit breaker for external calls | GOOD | Implemented |
|
||||
| Tool registration outside DI container | MEDIUM | Issue A above |
|
||||
| No integration tests for auth flow | MEDIUM | Issue F above |
|
||||
| Missing `composer.lock` | LOW | Issue D above |
|
||||
| `INFORMATION_SCHEMA` access blocked | GOOD | Implemented |
|
||||
| System table access blocked | GOOD | Implemented |
|
||||
|
||||
---
|
||||
|
||||
## 7. Phased Work Recommendations
|
||||
|
||||
### Phase 1 — Unblock Testing (Prerequisite)
|
||||
|
||||
1. Resolve `host-uk/core` dependency access (private registry credentials or mock stubs)
|
||||
2. Generate `composer.lock` after successful install
|
||||
3. Run `vendor/bin/pest` to establish a numerical test baseline
|
||||
|
||||
### Phase 2 — Critical Gaps
|
||||
|
||||
1. **Extract tools from `McpAgentServerCommand`** into the `McpToolsRegistering` event
|
||||
handler in `Boot::onMcpTools()` — makes the command testable
|
||||
2. **Write `QueryDatabase` tool tests** — primary public surface has zero test coverage
|
||||
3. **Write `AuditLogService` tests** — tamper-evident logging is security-critical
|
||||
4. **Write integration tests** for the full HTTP → tool → response flow
|
||||
|
||||
### Phase 3 — Code Quality
|
||||
|
||||
1. Fix estimated PHPStan level 5 type errors (see §4)
|
||||
2. Add `phpstan.neon` configuration file (currently absent)
|
||||
3. Add `pint.json` configuration file (currently absent)
|
||||
4. Resolve TODO.md items marked medium priority
|
||||
|
||||
### Phase 4 — Features
|
||||
|
||||
Refer to `TODO.md` for the full backlog.
|
||||
53
TODO.md
53
TODO.md
|
|
@ -1,5 +1,58 @@
|
|||
# Core-MCP TODO
|
||||
|
||||
> See [FINDINGS.md](FINDINGS.md) for the full Phase 0 environment assessment report.
|
||||
|
||||
## Phase 0 — Environment Blockers (February 2026)
|
||||
|
||||
- [ ] **Resolve `host-uk/core` dependency access**
|
||||
- Package is not available via Packagist; private registry credentials needed
|
||||
- Blocks: `composer install`, all tests, lint, and static analysis
|
||||
- **Action:** Configure private composer repository or provide mock stubs
|
||||
|
||||
- [ ] **Generate `composer.lock`** after successful install
|
||||
- Currently absent — dependency versions are unpinned
|
||||
- Reproducibility risk in CI/CD
|
||||
|
||||
- [ ] **Establish numeric test baseline**
|
||||
- Run `vendor/bin/pest` and record pass/fail counts
|
||||
- Targeted after dependency access is resolved
|
||||
|
||||
- [ ] **Run PHPStan analysis**
|
||||
- `vendor/bin/phpstan analyse --memory-limit=512M`
|
||||
- No `phpstan.neon` config file present — needs creating
|
||||
|
||||
- [ ] **Run lint baseline**
|
||||
- `vendor/bin/pint --test`
|
||||
- No `pint.json` config file present — needs creating
|
||||
|
||||
## Phase 1 — Critical Architecture (Prerequisite)
|
||||
|
||||
- [ ] **Refactor: Extract tools from `McpAgentServerCommand`**
|
||||
- MCP tools are registered inside the command, not via DI container
|
||||
- Implement `Boot::onMcpTools()` handler (currently a stub)
|
||||
- Enables unit testing of individual tools in isolation
|
||||
- **Files:** `src/Mcp/Boot.php`, `src/Mcp/Console/Commands/McpAgentServerCommand.php`
|
||||
- **Estimated effort:** 4-6 hours
|
||||
|
||||
- [ ] **Test Coverage: QueryDatabase Tool** — primary public surface has zero tests
|
||||
- Test SELECT execution, EXPLAIN analysis, connection validation
|
||||
- Test blocked keywords and injection prevention end-to-end
|
||||
- Test tier-based row limit truncation
|
||||
- Test timeout handling
|
||||
- **Files:** `tests/Unit/QueryDatabaseTest.php`
|
||||
- **Estimated effort:** 3-4 hours
|
||||
|
||||
- [ ] **Test Coverage: AuditLogService** — security-critical, no tests exist
|
||||
- Test HMAC tamper-evident logging
|
||||
- Test log verification command (`mcp:verify-audit-log`)
|
||||
- **Files:** `src/Mcp/Tests/Unit/AuditLogServiceTest.php`
|
||||
- **Estimated effort:** 2-3 hours
|
||||
|
||||
- [ ] **Add integration tests** — `tests/Feature/` is currently empty
|
||||
- Test full HTTP → tool → response flow
|
||||
- Test authentication and quota enforcement via middleware stack
|
||||
- **Estimated effort:** 4-5 hours
|
||||
|
||||
## Testing & Quality Assurance
|
||||
|
||||
### High Priority
|
||||
|
|
|
|||
52
docs/discovery-2026-02-21.md
Normal file
52
docs/discovery-2026-02-21.md
Normal 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**
|
||||
|
|
@ -8,6 +8,7 @@ return new class extends Migration
|
|||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('mcp_api_requests')) {
|
||||
Schema::create('mcp_api_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('request_id', 32)->unique();
|
||||
|
|
@ -32,6 +33,7 @@ return new class extends Migration
|
|||
$table->index('response_status');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ return new class extends Migration
|
|||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('mcp_tool_metrics')) {
|
||||
Schema::create('mcp_tool_metrics', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('tool_name');
|
||||
|
|
@ -24,8 +25,10 @@ return new class extends Migration
|
|||
$table->index(['date', 'tool_name']);
|
||||
$table->index('workspace_id');
|
||||
});
|
||||
}
|
||||
|
||||
// Table for tracking tool combinations (tools used together in sessions)
|
||||
if (! Schema::hasTable('mcp_tool_combinations')) {
|
||||
Schema::create('mcp_tool_combinations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('tool_a');
|
||||
|
|
@ -39,6 +42,7 @@ return new class extends Migration
|
|||
$table->index(['date', 'occurrence_count']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ return new class extends Migration
|
|||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('mcp_usage_quotas')) {
|
||||
Schema::create('mcp_usage_quotas', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
|
|
@ -21,6 +22,7 @@ return new class extends Migration
|
|||
$table->index('month');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ return new class extends Migration
|
|||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('mcp_audit_logs')) {
|
||||
Schema::create('mcp_audit_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
|
|
@ -58,8 +59,10 @@ return new class extends Migration
|
|||
$table->index(['is_sensitive', 'created_at']);
|
||||
$table->index(['actor_type', 'actor_id']);
|
||||
});
|
||||
}
|
||||
|
||||
// Table for tracking sensitive tool definitions
|
||||
if (! Schema::hasTable('mcp_sensitive_tools')) {
|
||||
Schema::create('mcp_sensitive_tools', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('tool_name')->unique();
|
||||
|
|
@ -69,6 +72,7 @@ return new class extends Migration
|
|||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ return new class extends Migration
|
|||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('mcp_tool_versions')) {
|
||||
Schema::create('mcp_tool_versions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('server_id', 64)->index();
|
||||
|
|
@ -33,6 +34,7 @@ return new class extends Migration
|
|||
$table->index(['deprecated_at', 'sunset_at'], 'mcp_tool_versions_lifecycle');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue