Merges feat/agent-project-membership (task 166/152). An agent can finally join many projects: membership routes, per-membership role and optional brain override, chat resolving context from the membership, and CLI attach-agent / detach-agent / get memberships. The legacy Agent.projectId fallback is kept for this release as the schema comment promises. Conflict resolved in main.ts: that branch added its RBAC case to mapUrlToPermission inline, while this branch had extracted the function to middleware/url-permission.ts so it could be tested. Kept the extraction and ported the case into it — joining a team is `edit:agents:<name>`, not `create:agents`, which is the authority to mint a whole agent. Also fixes something the extraction makes possible. security.test.ts contained a hand-copied reproduction of mapUrlToPermission, because main.ts self-invokes main() and nothing in it was importable. A security test asserting against a copy stops testing the system the moment the real one changes, and this copy had already drifted. It now imports the real function; all 18 assertions still pass, against the code that actually runs. Local: build clean, workspace 2550 passed, lint unchanged at 794 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
4.6 KiB
4.6 KiB
Task ID: 27
Title: Implement mcplocal management proxy routes
Status: done
Dependencies: 26 ✓
Priority: high
Description: Add REST endpoints to mcplocal that mirror mcpd's API and proxy management requests to mcpd without LLM processing. All requests must include proper authentication to mcpd using a Bearer token read from mcplocal config.
Details:
- Create HTTP client for mcpd communication with auth:
src/local-proxy/src/http/mcpd-client.tsexport class McpdClient { private token: string; constructor(private baseUrl: string, token: string) { this.token = token; } private getHeaders(): Record<string, string> { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` }; } async get<T>(path: string): Promise<T> { const response = await fetch(`${this.baseUrl}${path}`, { method: 'GET', headers: this.getHeaders() }); await this.handleAuthError(response); return response.json(); } async post<T>(path: string, body: unknown): Promise<T> { const response = await fetch(`${this.baseUrl}${path}`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(body) }); await this.handleAuthError(response); return response.json(); } async put<T>(path: string, body: unknown): Promise<T> { const response = await fetch(`${this.baseUrl}${path}`, { method: 'PUT', headers: this.getHeaders(), body: JSON.stringify(body) }); await this.handleAuthError(response); return response.json(); } async delete<T>(path: string): Promise<T> { const response = await fetch(`${this.baseUrl}${path}`, { method: 'DELETE', headers: this.getHeaders() }); await this.handleAuthError(response); return response.json(); } private async handleAuthError(response: Response): Promise<void> { if (response.status === 401) { throw new AuthenticationError('Invalid or expired token. Please check mcplocal config.'); } } } export class AuthenticationError extends Error { constructor(message: string) { super(message); this.name = 'AuthenticationError'; } } - Add token to mcplocal config type (extend ProxyConfig or similar):
export interface McpdAuthConfig { /** Bearer token for mcpd API authentication */ mcpdToken: string; } - Create proxy routes in
src/local-proxy/src/http/routes/:servers.ts: GET/POST /api/v1/servers, GET/PUT/DELETE /api/v1/servers/:idprofiles.ts: GET/POST /api/v1/profiles, GET/PUT/DELETE /api/v1/profiles/:idinstances.ts: GET/POST /api/v1/instances, GET/POST/DELETE /api/v1/instances/:id, etc.projects.ts: GET/POST /api/v1/projects, etc.audit.ts: GET /api/v1/audit-logsbackup.ts: POST /api/v1/backup, POST /api/v1/restore
- Each route handler forwards to mcpd with auth:
app.get('/api/v1/servers', async (req, reply) => { try { const result = await mcpdClient.get('/api/v1/servers'); return result; } catch (error) { if (error instanceof AuthenticationError) { return reply.status(401).send({ error: error.message }); } throw error; } }); - Add comprehensive error handling:
- If mcpd is unreachable, return 503 Service Unavailable
- If mcpd returns 401, return 401 with clear message about token configuration
- Forward other HTTP errors from mcpd with appropriate status codes
- Add request/response logging for debugging
Test Strategy:
- Unit test: McpdClient attaches Authorization header to all request methods (GET, POST, PUT, DELETE)
- Unit test: McpdClient throws AuthenticationError on 401 response from mcpd
- Unit test: Each proxy route forwards requests correctly with auth headers
- Unit test: Error handling when mcpd is unreachable (503 response)
- Unit test: Error handling when mcpd returns 401 (clear error message returned)
- Integration test: Full request flow mcpctl -> mcplocal -> mcpd with valid token
- Integration test: Full request flow with invalid token returns 401
- Test query parameters are forwarded correctly
- Test request body is forwarded correctly for POST/PUT
- Test path parameters (:id) are passed through correctly
- Mock mcpd responses and verify mcplocal returns them unchanged
- Test token is read correctly from mcplocal config