feat(ops): add workspace MCP deployment overlays
This commit is contained in:
+1389
File diff suppressed because it is too large
Load Diff
+81
@@ -0,0 +1,81 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
|
||||
import { DatabaseNotConfiguredError } from "../db/pool.js";
|
||||
import type { AgentsRepository } from "../repositories/agents.js";
|
||||
import { parseBearerToken } from "../security/bearer.js";
|
||||
|
||||
const MCP_PROTOCOL_VERSION = "2025-06-18";
|
||||
|
||||
type OntologyRouteDeps = {
|
||||
agentsRepository: AgentsRepository | null;
|
||||
ontologyCoreUrl: string;
|
||||
ontologyCoreAccessToken?: string;
|
||||
};
|
||||
|
||||
export async function registerOntologyGatewayRoutes(app: FastifyInstance, deps: OntologyRouteDeps): Promise<void> {
|
||||
app.post("/ontology-mcp", async (request, reply) => {
|
||||
if (!deps.agentsRepository) {
|
||||
throw new DatabaseNotConfiguredError();
|
||||
}
|
||||
|
||||
const token = parseBearerToken(request.headers.authorization);
|
||||
const session = await deps.agentsRepository.findActiveSessionByToken(token, "ontology");
|
||||
if (!session) {
|
||||
return reply.status(401).send({
|
||||
ok: false,
|
||||
error: "ontology_agent_unauthorized",
|
||||
message: "Ontology token is inactive, expired, revoked, or has the wrong purpose.",
|
||||
});
|
||||
}
|
||||
|
||||
const upstreamToken = deps.ontologyCoreAccessToken;
|
||||
if (!upstreamToken) {
|
||||
return reply.status(503).send({
|
||||
ok: false,
|
||||
error: "ontology_proxy_not_configured",
|
||||
message: "Ontology Core access token is not configured.",
|
||||
});
|
||||
}
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(new URL("/mcp", deps.ontologyCoreUrl), {
|
||||
method: "POST",
|
||||
redirect: "manual",
|
||||
headers: {
|
||||
Accept: readHeader(request.headers.accept) ?? "application/json, text/event-stream",
|
||||
Authorization: `Bearer ${upstreamToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"MCP-Protocol-Version": readHeader(request.headers["mcp-protocol-version"]) ?? MCP_PROTOCOL_VERSION,
|
||||
},
|
||||
body: JSON.stringify(request.body ?? {}),
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
} catch {
|
||||
return reply.status(503).send({
|
||||
ok: false,
|
||||
error: "ontology_proxy_unavailable",
|
||||
message: "Ontology Core MCP is unavailable.",
|
||||
});
|
||||
}
|
||||
|
||||
const payload = await upstream.text();
|
||||
reply.status(upstream.status);
|
||||
reply.header("Cache-Control", "no-store");
|
||||
reply.header("Content-Type", upstream.headers.get("content-type") ?? "application/json; charset=utf-8");
|
||||
for (const header of ["mcp-protocol-version", "mcp-session-id", "vary"]) {
|
||||
const value = upstream.headers.get(header);
|
||||
if (value) {
|
||||
reply.header(header, value);
|
||||
}
|
||||
}
|
||||
return reply.send(payload);
|
||||
});
|
||||
}
|
||||
|
||||
function readHeader(value: string | string[] | undefined): string | undefined {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
|
||||
import { executeMcpTool } from "../mcp/tool-runtime.js";
|
||||
import type { AgentsRepository } from "../repositories/agents.js";
|
||||
import type { TaskerClient } from "../tasker/client.js";
|
||||
import { authenticateAgent } from "./session.js";
|
||||
|
||||
type ToolRouteDeps = {
|
||||
agentsRepository: AgentsRepository | null;
|
||||
taskerClient: TaskerClient;
|
||||
};
|
||||
|
||||
export async function registerToolRoutes(app: FastifyInstance, deps: ToolRouteDeps): Promise<void> {
|
||||
app.post("/api/v1/tools/projects", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const result = await executeMcpTool(session, "tasker_create_project", request.body, deps, toolOptions(request));
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.get("/api/v1/tools/projects", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const result = await executeMcpTool(session, "tasker_list_projects", {}, deps, toolOptions(request));
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.get("/api/v1/tools/projects/:projectId/context", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { projectId: string };
|
||||
const query = request.query as { workspace_slug?: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_get_project_context",
|
||||
{
|
||||
project_id: params.projectId,
|
||||
workspace_slug: query.workspace_slug,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.post("/api/v1/tools/projects/:projectId/labels/ensure", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { projectId: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_ensure_labels",
|
||||
{
|
||||
...requestBodyRecord(request.body),
|
||||
project_id: params.projectId,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.get("/api/v1/tools/issues", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const query = request.query as { project_id?: string; workspace_slug?: string; query?: string };
|
||||
const result = await executeMcpTool(session, "tasker_search_issues", query, deps, toolOptions(request));
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.post("/api/v1/tools/issues", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const result = await executeMcpTool(session, "tasker_create_issue", request.body, deps, toolOptions(request));
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.get("/api/v1/tools/issues/:issueId", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { issueId: string };
|
||||
const query = request.query as { project_id?: string; workspace_slug?: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_get_issue",
|
||||
{ ...query, issue_id: params.issueId },
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.patch("/api/v1/tools/issues/:issueId", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { issueId: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_update_issue",
|
||||
{
|
||||
...requestBodyRecord(request.body),
|
||||
issue_id: params.issueId,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.post("/api/v1/tools/issues/:issueId/move", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { issueId: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_move_issue",
|
||||
{
|
||||
...requestBodyRecord(request.body),
|
||||
issue_id: params.issueId,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.post("/api/v1/tools/issues/:issueId/comments", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { issueId: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_append_comment",
|
||||
{
|
||||
...requestBodyRecord(request.body),
|
||||
issue_id: params.issueId,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.post("/api/v1/tools/issues/:issueId/attachments", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { issueId: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_attach_file",
|
||||
{
|
||||
...requestBodyRecord(request.body),
|
||||
issue_id: params.issueId,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.put("/api/v1/tools/issues/:issueId/labels", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { issueId: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_set_issue_labels",
|
||||
{
|
||||
...requestBodyRecord(request.body),
|
||||
issue_id: params.issueId,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
|
||||
app.put("/api/v1/tools/issues/:issueId/assignees", async (request) => {
|
||||
const session = await authenticateAgent(request, deps);
|
||||
const params = request.params as { issueId: string };
|
||||
const result = await executeMcpTool(
|
||||
session,
|
||||
"tasker_assign_issue",
|
||||
{
|
||||
...requestBodyRecord(request.body),
|
||||
issue_id: params.issueId,
|
||||
},
|
||||
deps,
|
||||
toolOptions(request)
|
||||
);
|
||||
return result.structuredContent;
|
||||
});
|
||||
}
|
||||
|
||||
function toolOptions(request: { headers: Record<string, string | string[] | undefined> }): { source: "rest"; idempotencyKey: string | null } {
|
||||
return {
|
||||
source: "rest",
|
||||
idempotencyKey: readHeader(request.headers["idempotency-key"]),
|
||||
};
|
||||
}
|
||||
|
||||
function requestBodyRecord(body: unknown): Record<string, unknown> {
|
||||
if (typeof body === "object" && body !== null && !Array.isArray(body)) {
|
||||
return body as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function readHeader(value: string | string[] | undefined): string | null {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? null;
|
||||
}
|
||||
|
||||
return value ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user