import { Pool } from "pg"; import { buildApp } from "../app.js"; import { loadConfig } from "../config.js"; import { runMigrations } from "../db/migrations.js"; const envWithoutTaskerToken = { ...process.env }; delete envWithoutTaskerToken.NODEDC_INTERNAL_ACCESS_TOKEN; envWithoutTaskerToken.LOG_LEVEL = "silent"; const config = loadConfig(envWithoutTaskerToken); if (!config.DATABASE_URL) { throw new Error("DATABASE_URL is required for MCP smoke test."); } const migrationPool = new Pool({ connectionString: config.DATABASE_URL }); await runMigrations(migrationPool); await migrationPool.end(); const app = await buildApp(config); try { const suffix = Date.now().toString(36); const projectId = `mcp-project-${suffix}`; const workspaceSlug = `mcp-workspace-${suffix}`; const agentId = await createAgent(suffix); await upsertGrant(agentId, workspaceSlug, projectId, ["workspace:read", "project:read", "issue:read"]); const token = await createToken(agentId); const headers = { Authorization: `Bearer ${token}`, Accept: "application/json, text/event-stream", "MCP-Protocol-Version": "2025-06-18", }; const initialize = await mcpRequest(1, "initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "nodedc-smoke", version: "0.1.0", }, }); assert(initialize.result?.capabilities?.tools, "initialize exposes tools capability"); const toolsList = await mcpRequest(2, "tools/list", {}, headers); const toolNames = toolsList.result.tools.map((tool: { name: string }) => tool.name); assert(toolNames.includes("tasker_get_agent_instructions"), "instructions tool is listed"); assert(!toolNames.includes("tasker_create_issue"), "create tool is hidden without issue:create scope"); const instructions = await mcpRequest( 3, "tools/call", { name: "tasker_get_agent_instructions", arguments: {}, }, headers ); assert(instructions.result?.structuredContent?.rules, "instructions tool returns structured rules"); const deniedCreate = await mcpRequest( 4, "tools/call", { name: "tasker_create_issue", arguments: { project_id: projectId, workspace_slug: workspaceSlug, title: "Should be denied by MCP smoke", idempotency_key: `mcp-denied-create-${suffix}`, }, }, headers ); assert(deniedCreate.result?.isError === true, "create call returns MCP tool error without issue:create scope"); const missingIdempotencyKey = await mcpRequest( 5, "tools/call", { name: "tasker_create_issue", arguments: { project_id: projectId, workspace_slug: workspaceSlug, title: "Should be denied before write because idempotency key is missing", }, }, headers ); assert( missingIdempotencyKey.result?.structuredContent?.error === "idempotency_key_required", "write tools require idempotency key" ); await upsertGrant(agentId, workspaceSlug, projectId, ["workspace:read", "project:read", "issue:read", "issue:update"]); await upsertGrant(agentId, workspaceSlug, `${projectId}-structured`, [ "workspace:read", "project:read", "issue:structured_blocks:write", ]); const deniedSplitGrantStructuredUpdate = await mcpRequest( 6, "tools/call", { name: "tasker_update_structured_blocks", arguments: { issue_id: "split-grant-issue", project_id: projectId, workspace_slug: workspaceSlug, structured_blocks: [], idempotency_key: `mcp-split-grant-${suffix}`, }, }, headers ); assert( deniedSplitGrantStructuredUpdate.result?.structuredContent?.error === "forbidden", "structured block writes require both scopes on the same project grant" ); await upsertGrant(agentId, workspaceSlug, projectId, ["workspace:read", "project:read", "issue:read", "issue:create"]); const allowedButNoAdapter = await mcpRequest( 7, "tools/call", { name: "tasker_create_issue", arguments: { project_id: projectId, workspace_slug: workspaceSlug, title: "Allowed by MCP, waiting for Tasker adapter token", idempotency_key: `mcp-allowed-no-adapter-${suffix}`, }, }, headers ); assert(allowedButNoAdapter.result?.isError === true, "allowed create reaches Tasker boundary"); assert( allowedButNoAdapter.result?.structuredContent?.error === "TaskerAdapterNotConfiguredError", "Tasker boundary error is explicit" ); console.log( JSON.stringify( { ok: true, agent_id: agentId, token_prefix: token.split("_")[0], tools_listed: toolNames.length, checks: { initialize: "passed", tools_list: "passed", instructions_call: "passed", denied_without_scope: "passed", idempotency_required: "passed", denied_split_grant_structured_blocks: "passed", allowed_request_reaches_tasker_boundary: "passed", }, }, null, 2 ) ); } finally { await app.close(); } async function createAgent(suffix: string): Promise { const response = await app.inject({ method: "POST", url: "/api/v1/agents", payload: { owner_user_id: `mcp-owner-${suffix}`, owner_email: `mcp-${suffix}@example.test`, display_name: `MCP Codex ${suffix}`, }, }); assertStatus(response.statusCode, 201, response.body); return JSON.parse(response.body).agent.id; } async function upsertGrant(agentId: string, workspaceSlug: string, projectId: string, scopes: string[]): Promise { const response = await app.inject({ method: "POST", url: `/api/v1/agents/${agentId}/grants`, payload: { workspace_slug: workspaceSlug, project_id: projectId, scopes, mode: "voluntary", created_by_user_id: "mcp-smoke-admin", }, }); assertStatus(response.statusCode, 201, response.body); } async function createToken(agentId: string): Promise { const response = await app.inject({ method: "POST", url: `/api/v1/agents/${agentId}/tokens`, payload: { name: "MCP smoke token", }, }); assertStatus(response.statusCode, 201, response.body); return JSON.parse(response.body).token; } async function mcpRequest(id: number, method: string, params?: unknown, headers?: Record): Promise { const response = await app.inject({ method: "POST", url: "/mcp", headers, payload: { jsonrpc: "2.0", id, method, params, }, }); assertStatus(response.statusCode, 200, response.body); const payload = JSON.parse(response.body); if (payload.error) { throw new Error(`MCP error for ${method}: ${JSON.stringify(payload.error)}`); } return payload; } function assertStatus(actual: number, expected: number, body: string): void { if (actual !== expected) { throw new Error(`Expected HTTP ${expected}, received HTTP ${actual}: ${body}`); } } function assert(condition: unknown, message: string): void { if (!condition) { throw new Error(`MCP smoke assertion failed: ${message}`); } }