131 lines
4.6 KiB
Markdown
131 lines
4.6 KiB
Markdown
|
|
# 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:**
|
||
|
|
|
||
|
|
1. Create HTTP client for mcpd communication with auth: `src/local-proxy/src/http/mcpd-client.ts`
|
||
|
|
```typescript
|
||
|
|
export 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';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
2. Add token to mcplocal config type (extend ProxyConfig or similar):
|
||
|
|
```typescript
|
||
|
|
export interface McpdAuthConfig {
|
||
|
|
/** Bearer token for mcpd API authentication */
|
||
|
|
mcpdToken: string;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
3. Create proxy routes in `src/local-proxy/src/http/routes/`:
|
||
|
|
- `servers.ts`: GET/POST /api/v1/servers, GET/PUT/DELETE /api/v1/servers/:id
|
||
|
|
- `profiles.ts`: GET/POST /api/v1/profiles, GET/PUT/DELETE /api/v1/profiles/:id
|
||
|
|
- `instances.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-logs
|
||
|
|
- `backup.ts`: POST /api/v1/backup, POST /api/v1/restore
|
||
|
|
4. Each route handler forwards to mcpd with auth:
|
||
|
|
```typescript
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
```
|
||
|
|
5. 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
|
||
|
|
6. Add request/response logging for debugging
|
||
|
|
|
||
|
|
**Test Strategy:**
|
||
|
|
|
||
|
|
1. Unit test: McpdClient attaches Authorization header to all request methods (GET, POST, PUT, DELETE)
|
||
|
|
2. Unit test: McpdClient throws AuthenticationError on 401 response from mcpd
|
||
|
|
3. Unit test: Each proxy route forwards requests correctly with auth headers
|
||
|
|
4. Unit test: Error handling when mcpd is unreachable (503 response)
|
||
|
|
5. Unit test: Error handling when mcpd returns 401 (clear error message returned)
|
||
|
|
6. Integration test: Full request flow mcpctl -> mcplocal -> mcpd with valid token
|
||
|
|
7. Integration test: Full request flow with invalid token returns 401
|
||
|
|
8. Test query parameters are forwarded correctly
|
||
|
|
9. Test request body is forwarded correctly for POST/PUT
|
||
|
|
10. Test path parameters (:id) are passed through correctly
|
||
|
|
11. Mock mcpd responses and verify mcplocal returns them unchanged
|
||
|
|
12. Test token is read correctly from mcplocal config
|