Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions docs/MCP_SERVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,21 @@ Do not request `tasks:admin` for personal planning. Public manual search and pub

Task-listing tools take `visibility: "all" | "public" | "private"` — signed-in callers default to `all` (public plus their own private work). On the tools that previously exposed `scope`/`taskScope`, `"accessible"` survives as a deprecated alias for `"all"`; `listTasks` never had the alias and takes only `visibility`.

Example private task:
Example private task (use the `personalRoot.id` returned by `getMe`):

```json
{
"title": "File federal taxes",
"description": "Prepare, review, and file the federal and state returns before the legal deadline.",
"parentTaskId": "<personalRoot.id from getMe>",
"taskKey": "personal:taxes:2026",
"category": "OTHER",
"hours": 3,
"value": 20000,
"p_success": 0.99,
"cash_cost": 150,
"expected_deliverable": "Accepted federal and state returns with filing receipts",
"acceptance_criteria": [
"impactStatement": "Filing correctly and on time avoids penalties, interest, and account disruption.",
"acceptanceCriteria": [
"Return totals match reviewed source documents",
"Filing receipt is attached"
],
Expand All @@ -120,6 +124,45 @@ Example private task:
}
```

## Creating Optimitron Development Tasks

Improvements to Optimitron itself belong under the managed development root
whose stable task key is `optimitron:dev`. Do not attach them directly to
Optimize Earth and do not rely on a memorized database ID:

1. Call `getMe` and confirm the connected identity has access to the
development tree.
2. Call `searchTasks` with `query: "optimitron:dev"` and
`visibility: "all"`.
3. Select the exact `taskKey: "optimitron:dev"` result. Search the proposed
title and its distinctive terms to avoid duplicating existing work.
4. Call `createTask` with `parentTaskKey: "optimitron:dev"`; use an
`optimitron:dev:<short-slug>` key for the new task.

If the exact development root is not returned, stop and report the access or
search problem. Do not substitute the Optimize Earth root or a merely similar
task.

```json
{
"title": "Make development-parent discovery reliable in MCP",
"description": "Let agents find the canonical development root with natural task-search queries and document the exact creation flow.",
"parentTaskKey": "optimitron:dev",
"taskKey": "optimitron:dev:mcp-development-parent-discovery",
"category": "ENGINEERING",
"hours": 4,
"value": 20000,
"p_success": 0.9,
"impactStatement": "Reliable parent discovery prevents orphaned and duplicate development work.",
"acceptanceCriteria": [
"Natural multi-word searches return the development root",
"The MCP instructions show the exact search-then-create flow",
"Regression tests cover development-root discovery"
],
"executor_type": "AI Agent"
}
```

## Estimate Standards For Agents

For public Earth-level tasks, agents should use `setTaskImpact` when they know enough to estimate value. For private personal tasks, prefer `createTask` / `updateTask` with `hours`, `value`, `p_success`, and `cash_cost`. If an agent does not know enough to estimate value, it should create a clarification/decomposition task rather than inventing confidence.
Expand Down Expand Up @@ -188,7 +231,7 @@ admin gating — is the **[MCP Tool Reference](https://optimitron.com/developers
(human) and `/api/mcp/tools` (machine). When this section and those sources
disagree, those sources win.

- Queue discovery: `listTasks`, `getTask`, `getBlockers`, `getQueueAudit`, `getNextAction`, `getMyQueue`, `getAIQueue`, `evaluateTaskEconomics`.
- Queue discovery: `listTasks`, `searchTasks`, `getTask`, `getBlockers`, `getQueueAudit`, `getNextAction`, `getMyQueue`, `getAIQueue`, `evaluateTaskEconomics`.
- Personal task management: `createTask`, `updateTask`, `deleteTask`, `proposeTaskBundle` (multi-task drafts with duplicate review), `promoteTask` (DRAFT → ACTIVE after review).
- Reviewed private import: `reviewPrivateTaskBundle`, `applyPrivateTaskBundle`, `deletePrivateSourceSelection`.
- Private execution (admin/agent-gated; not exposed to ordinary third-party tokens): `startTaskExecution`, `submitTaskArtifact`, `submitTaskForVerification`, `verifyTaskExecution`, `getTaskAuditTrail`.
Expand Down
175 changes: 175 additions & 0 deletions packages/web/src/lib/__tests__/mcp-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DOCUMENT_REVIEW_TASK_KEY_PREFIX } from "../tasks/document-review-contra

const mocks = vi.hoisted(() => ({
listTasks: vi.fn(),
searchTasks: vi.fn(),
getTaskDetailData: vi.fn(),
claimTask: vi.fn(),
completeSelfTask: vi.fn(),
Expand Down Expand Up @@ -172,6 +173,7 @@ vi.mock("../tasks.server", () => ({
completeSelfTask: mocks.completeSelfTask,
completeTaskClaim: mocks.completeTaskClaim,
listTasks: mocks.listTasks,
searchTasks: mocks.searchTasks,
getTaskDetailData: mocks.getTaskDetailData,
}));

Expand Down Expand Up @@ -715,6 +717,7 @@ beforeEach(() => {
}),
);
mocks.listTasks.mockResolvedValue([]);
mocks.searchTasks.mockResolvedValue([]);
mocks.claimTask.mockResolvedValue({
id: "claim-1",
status: TaskClaimStatus.CLAIMED,
Expand Down Expand Up @@ -2196,6 +2199,52 @@ describe("MCP server tool dispatch", () => {
});
});

it("routes natural development-parent searches through the authenticated task boundary", async () => {
mocks.searchTasks.mockResolvedValue([
{
id: "optimitron-dev",
taskKey: "optimitron:dev",
title: "Optimize Optimitron: engineering program",
},
]);

const client = await setup("user-1", ALL_SCOPES);
const tools = await client.listTools();
const searchTool = tools.tools.find(
(tool) => tool.name === "searchTasks",
);
const createTool = tools.tools.find((tool) => tool.name === "createTask");
expect(searchTool?.description).toContain("optimitron:dev");
expect(createTool?.description).toContain("optimitron:dev");

const result = await client.callTool({
name: "searchTasks",
arguments: {
query: "find Optimize Optimitron parent",
status: "ACTIVE",
visibility: "all",
},
});

expect(result.isError).toBeFalsy();
expect(mocks.searchTasks).toHaveBeenCalledWith(
"find Optimize Optimitron parent",
expect.objectContaining({
clientAccessBoundary: expect.any(Object),
limit: 20,
status: "ACTIVE",
userId: "user-1",
visibility: "accessible",
}),
);
expect(parseToolBody(result)).toEqual([
expect.objectContaining({
id: "optimitron-dev",
taskKey: "optimitron:dev",
}),
]);
});

it("does not invent private visibility when listTasks omits isPublic", async () => {
mocks.listTasks.mockResolvedValue([
makeCreatedTask({
Expand Down Expand Up @@ -4459,6 +4508,21 @@ describe("MCP server tool dispatch", () => {
expect(mocks.taskCreate).not.toHaveBeenCalled();
});

it("rejects ambiguous parent id and key inputs", async () => {
const client = await setup("user-1", ALL_SCOPES);
const result = await client.callTool({
name: "createTask",
arguments: makeCreateTaskArguments({
parentTaskKey: "optimitron:dev",
}),
});

expect(result.isError).toBe(true);
expect(parseToolBody(result).message).toContain("provide exactly one");
expect(mocks.taskFindFirst).not.toHaveBeenCalled();
expect(mocks.taskCreate).not.toHaveBeenCalled();
});

it("rejects Optimize Earth as a direct task parent", async () => {
const client = await setup("user-1", ALL_SCOPES);
const result = await client.callTool({
Expand All @@ -4484,6 +4548,25 @@ describe("MCP server tool dispatch", () => {
expect(mocks.taskCreate).not.toHaveBeenCalled();
});

it("rejects the Optimize Earth stable key as a direct task parent", async () => {
mocks.taskFindFirst.mockResolvedValue(makeOptimizeEarthRoot());
const { parentTaskId: _parentTaskId, ...argumentsWithoutParentId } =
makeCreateTaskArguments({
parentTaskKey: "program:optimize-earth",
});
const client = await setup("user-1", ALL_SCOPES);
const result = await client.callTool({
name: "createTask",
arguments: argumentsWithoutParentId,
});

expect(result.isError).toBe(true);
expect(parseToolBody(result).message).toContain(
"Optimize Earth is reserved",
);
expect(mocks.taskCreate).not.toHaveBeenCalled();
});

it("response includes missingFields[] for soft-recommended fields", async () => {
mocks.getTaskDetailData.mockResolvedValue({
task: makeCreatedTask({
Expand Down Expand Up @@ -5500,6 +5583,98 @@ describe("MCP server tool dispatch", () => {
expect(data.maxClaims).toBeNull();
});

it("createTask resolves an exact parentTaskKey without requiring an opaque parent id", async () => {
mocks.taskFindFirst.mockImplementation(
async (args?: { where?: { taskKey?: string } }) => {
if (args?.where?.taskKey === "optimitron:dev") {
return makePlanningBranch({
id: "optimitron-dev",
taskKey: "optimitron:dev",
});
}
return null;
},
);
mocks.getTaskDetailData.mockResolvedValue({
task: makeCreatedTask({
id: "created-task",
parentTaskId: "optimitron-dev",
contextJson: {
executor_type: "AI Agent",
value: 100,
p_success: 0.5,
cash_cost: 0,
},
}),
});
mocks.computeTaskPriority.mockReturnValue(makePriority());
const { parentTaskId: _parentTaskId, ...argumentsWithoutParentId } =
makeCreateTaskArguments({
executor_type: "AI Agent",
parentTaskKey: "optimitron:dev",
});

const client = await setup("user-1", ALL_SCOPES);
const result = await client.callTool({
name: "createTask",
arguments: argumentsWithoutParentId,
});

expect(result.isError).toBeFalsy();
expect(mocks.taskFindFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ taskKey: "optimitron:dev" }),
}),
);
const data = (
mocks.taskCreate.mock.calls[0]![0] as { data: Record<string, unknown> }
).data;
expect(data.parentTaskId).toBe("optimitron-dev");
expect(data).not.toHaveProperty("parentTaskKey");
});

it("scopes parentTaskKey resolution to the OAuth client boundary", async () => {
mocks.taskFindFirst.mockResolvedValue(null);
const clientBoundaryWhere = {
OR: [
{ isPublic: true },
{
isPublic: false,
ownerOrganizationId: { in: ["organization-1"] },
},
],
};
mocks.getTaskClientAccessWhere.mockReturnValue(clientBoundaryWhere);
const { parentTaskId: _parentTaskId, ...argumentsWithoutParentId } =
makeCreateTaskArguments({
parentTaskKey: "optimitron:dev",
});
const client = await setup("user-1", [McpScope.TASKS_ORGANIZATION], {
organizationIds: ["organization-1"],
});

const result = await client.callTool({
name: "createTask",
arguments: argumentsWithoutParentId,
});

expect(result.isError).toBe(true);
expect(parseToolBody(result).message).toContain("was not found");
expect(mocks.getTaskClientAccessWhere).toHaveBeenCalledWith({
allowPersonalPrivate: false,
organizationIds: ["organization-1"],
});
expect(mocks.taskFindFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
taskKey: "optimitron:dev",
AND: expect.arrayContaining([clientBoundaryWhere]),
}),
}),
);
expect(mocks.taskCreate).not.toHaveBeenCalled();
});

it("rejects updateTask when a non-admin user targets a public task", async () => {
mocks.getTaskDetailData.mockResolvedValue({
task: makeCreatedTask({
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/lib/__tests__/mcp-tool-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,8 @@ describe("MCP tool catalog", () => {

expect(phantom).toEqual([]);
});

it("tells coding agents how to target the canonical development branch", () => {
expect(MCP_SERVER_INSTRUCTIONS).toContain("parentTaskKey='optimitron:dev'");
});
});
Loading
Loading