Tasks MCP — Project Roadmap

Architecture Overview

How It Works

AshAi (ash_ai ~> 0.4) generates MCP-compliant tool schemas directly from Ash resource actions. The AshAi.Mcp.Router handles the MCP protocol (initialize, tools/list, tools/call) and is forwarded to at /mcp. Authentication is API-key-based via AshAuthentication.Strategy.ApiKey. All task operations are user-scoped — the API key identifies the user, and Ash policies enforce that agents can only touch that user's tasks.

POST /mcp            :mcp pipeline (ApiKey auth) AshAi.Mcp.Router
Authorization: Bearer <key>    resolves to Kacom.Accounts.User actor
tools/call create_task       Kacom.Tasks.Task :create (actor scoped)
tools/call update_task       Kacom.Tasks.Task :update (actor scoped)

Protocol

MCP 2024-11-05. Single endpoint POST /mcp. JSON-RPC envelope. Stateless — each call is authenticated independently.

Auth

API key in Authorization: Bearer header. Keys are hashed at rest. Scoped to one user. Generated and revoked at /account.

Authorization

Ash Policy Authorizer. Create: requires actor. Read/Update/Destroy: user_id == actor.id filter. Agents cannot touch other users' tasks.

Tool Generation

AshAi derives tool name, description, and JSON Schema from resource attributes and action accept lists. No hand-written tool schemas needed.

🔧 Current Tool Inventory

Tools live in two places: the tools do block in lib/kacom/tasks.ex (domain) and the tools: list in the forward "/", AshAi.Mcp.Router call in lib/kacom_web/router.ex. A tool must appear in both to be callable.

Tool Name Resource Action What It Does Exposed?
list_tasks Task :read Return all tasks for the authenticated user. Supports filtering. ✓ Yes
create_task Task :create Create a new task. Accepts title, description, category_id, status, priority, due_date. ✓ Yes
update_task Task :update Update any writable field including status. Can change status to any valid value. ✓ Yes
delete_task Task :destroy Permanently delete a task. ✓ Yes
list_categories Category :read List all categories for the user. ✓ Yes
create_category Category :create Create a new task category with name and color. ✓ Yes
complete_task Task :complete Set status→done, stamp completed_at. Semantic verb — clearer to agents than update_task. ✓ Yes
reopen_task Task :reopen Set status→todo, clear completed_at. Semantic verb. ✓ Yes
archive_task Task :archive Set status→archived. Semantic verb. ✓ Yes
Note: Status changes are technically possible today via update_task by passing status: "done" etc. However, exposing complete_task, reopen_task, and archive_task as dedicated tools gives agents unambiguous verbs, correctly stamps completed_at via the action's built-in change, and reduces the chance of an agent passing an invalid or unexpected status string.

Phases

Phase 1
Foundation

Goal: MCP endpoint live, authentication working, core CRUD tools exposed, account UI for key management.

✓ Complete

What Was Built

  • POST /mcpAshAi.Mcp.Router forwarded from the :mcp pipeline in lib/kacom_web/router.ex
  • :mcp pipeline — AshAuthentication.Strategy.ApiKey.Plug with resource: Kacom.Accounts.User, required?: true
  • 6 tools declared in lib/kacom/tasks.ex tools do block and listed in the router forward
  • Kacom.Accounts.ApiKey Ash resource — api_keys table, hashed storage, scoped to user
  • /account LiveView — Generate / Regenerate / Revoke UI; displays raw key once with copy-now warning
  • Protocol version 2024-11-05 declared on the router forward

Key Files

  • lib/kacom/tasks.ex — domain with tools do block
  • lib/kacom_web/router.ex:25-72:mcp pipeline + scope
  • lib/kacom/accounts/api_key.ex — ApiKey resource
  • lib/kacom_web/live/account_live.ex — key management UI

Deliverables

  • MCP endpoint at /mcp
  • API key authentication pipeline
  • 6 base tools: list_tasks, create_task, update_task, delete_task, list_categories, create_category
  • API key generation / revocation UI at /account
  • Ash Policy Authorizer — tasks scoped to actor
Phase 2
Status Transition Tools

Goal: expose complete_task, reopen_task, archive_task as dedicated MCP tools; verify actor flows correctly through the pipeline.

✓ Complete

Step 1 — Add Tools to Domain

In lib/kacom/tasks.ex, add three entries to the tools do block. The :complete, :reopen, and :archive actions already exist on the Task resource — they just need to be wired as tools:

# lib/kacom/tasks.ex  — tools do block
tool :complete_task,  Kacom.Tasks.Task, :complete
tool :reopen_task,    Kacom.Tasks.Task, :reopen
tool :archive_task,   Kacom.Tasks.Task, :archive

Step 2 — Add Tools to Router

In lib/kacom_web/router.ex, add the three new tool atoms to the tools: list on the AshAi.Mcp.Router forward:

# lib/kacom_web/router.ex:64-71
forward "/", AshAi.Mcp.Router,
  tools: [
    :list_tasks, :create_task, :update_task, :delete_task,
    :list_categories, :create_category,
    :complete_task, :reopen_task, :archive_task,   # <-- add
    :list_recipes, :create_recipe, :update_recipe, :delete_recipe
  ],
  protocol_version_statement: "2024-11-05",
  otp_app: :kacom

Step 3 — Verify Actor Context ✓ Resolved

Confirmed by reading AshAuthentication.Strategy.ApiKey.Plug source — line 123 calls Ash.PlugHelpers.set_actor(subject) directly after a successful key lookup. No additional plug :set_actor, :user needed in the :mcp pipeline. Actor is set before AshAi.Mcp.Router is reached.

Step 4 — Smoke Test All Status Tools

Using curl or a Claude Code MCP client, call each new tool and verify the database state:

# Initialize
curl -X POST http://localhost:4000/mcp \
  -H "Authorization: Bearer kacom_..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'

# List tools (confirm complete_task, reopen_task, archive_task appear)
curl -X POST http://localhost:4000/mcp \
  -H "Authorization: Bearer kacom_..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# Complete a task
curl -X POST http://localhost:4000/mcp \
  -H "Authorization: Bearer kacom_..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"complete_task","arguments":{"id":"<task-uuid>"}}}'

Deliverables

  • complete_task tool in domain + router
  • reopen_task tool in domain + router
  • archive_task tool in domain + router
  • Actor context verified — AshAuthentication.Strategy.ApiKey.Plug calls Ash.PlugHelpers.set_actor/1 directly
  • Smoke test passing for all 9 task tools
Phase 3
Agent Integration Config

Goal: Claude Desktop and Claude Code can connect to the MCP server out of the box using configs documented in the repo.

✓ Complete

Claude Code MCP Config

Claude Code reads MCP server config from ~/.claude/mcp_servers.json (global) or .mcp.json (project-local). Add a project-local .mcp.json in the repo root so any Claude Code session in this project can call the local dev MCP server automatically:

// .mcp.json  (project root — commit this without the real key; use env var)
{
  "mcpServers": {
    "kacom-tasks": {
      "type": "http",
      "url": "http://localhost:4000/mcp",
      "headers": {
        "Authorization": "Bearer ${KACOM_MCP_KEY}"
      }
    }
  }
}

Set KACOM_MCP_KEY in the shell before launching claude. The key is generated at /account. Production URL would be https://kyleaziz.com/mcp.

Claude Desktop Config

Claude Desktop reads from ~/Library/Application Support/Claude/claude_desktop_config.json on macOS. Add a kacom-tasks entry under mcpServers:

{
  "mcpServers": {
    "kacom-tasks": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://kyleaziz.com/mcp"],
      "env": {
        "MCP_REMOTE_HEADER_Authorization": "Bearer kacom_..."
      }
    }
  }
}

Claude Desktop does not support HTTP MCP servers natively yet (as of 2024-11); the mcp-remote bridge translates stdio ↔ HTTP. Once Claude Desktop adds native HTTP support, the config simplifies to the same shape as Claude Code.

Both configs are also documented in deploy/README.md in the repo root. Production URL: https://kyleaziz.com/mcp.

Tool Description Audit

AshAi auto-generates tool descriptions from resource attributes. Before going live with agents, verify the generated descriptions are clear enough for LLM tool selection. Run tools/list and read each description field. If any are ambiguous, add a description: option to the tool declaration in lib/kacom/tasks.ex:

# lib/kacom/tasks.ex — tools do block (description override example)
tool :complete_task, Kacom.Tasks.Task, :complete,
  description: "Mark a task as done and record completion time. Use this instead of update_task when the user says a task is finished."

Account Page Enhancement

The /account page already shows the API key once on generation. Enhance it to display the exact config snippet for Claude Code so users can copy-paste directly — no docs hunting required.

Deliverables

  • .mcp.json in repo root with Claude Code config (env-var key, no hardcoded secret)
  • Claude Desktop config snippet documented in deploy/README.md
  • Tool description audit — all 9 task tools have clear descriptions
  • /account page shows ready-to-paste Claude Code config after key generation
Phase 4
Production Hardening

Goal: the MCP endpoint is safe, observable, and resilient enough to leave running and connected to real AI agents indefinitely.

Upcoming

Rate Limiting

Add a rate limiter to the :mcp pipeline to prevent runaway agents from flooding the database. hammer (already common in Phoenix projects) or ex_rated work well with Plug:

# mix.exs
{:hammer, "~> 6.0"}

# lib/kacom_web/plugs/mcp_rate_limit.ex
defmodule KacomWeb.Plugs.McpRateLimit do
  import Plug.Conn
  def call(conn, _opts) do
    user_id = conn.assigns[:current_user].id
    case Hammer.check_rate("mcp:#{user_id}", 60_000, 60) do
      {:allow, _} -> conn
      {:deny, _}  -> conn |> put_status(429) |> Phoenix.Controller.json(%{error: "rate_limited"}) |> halt()
    end
  end
end

Logging & Observability

Log MCP tool calls with tool name, user id, and latency. A simple Plug in the :mcp pipeline is sufficient for v1. Later: pipe logs to your existing telemetry pipeline.

defmodule KacomWeb.Plugs.McpLogger do
  require Logger
  def call(conn, _opts) do
    start = System.monotonic_time(:millisecond)
    conn = Plug.Conn.register_before_send(conn, fn conn ->
      duration = System.monotonic_time(:millisecond) - start
      user_id = get_in(conn.assigns, [:current_user, :id]) || "unauthenticated"
      Logger.info("[MCP] user=#{user_id} status=#{conn.status} duration=#{duration}ms")
      conn
    end)
    conn
  end
end

HTTPS Enforcement

The API key is sent in plaintext headers — HTTPS is mandatory in production. Ensure the deploy config (Caddy / nginx / Fly.io) terminates TLS before /mcp traffic reaches Phoenix. Optionally add a plug that rejects non-HTTPS requests in prod:

# config/runtime.exs — already enforced by Phoenix.Endpoint force_ssl option if configured
config :kacom, KacomWeb.Endpoint,
  force_ssl: [rewrite_on: [:x_forwarded_proto]]

Key Naming / Labels

If a user wants to give one key to Claude Desktop and another to Claude Code, they need per-key labels. Add an optional label attribute to Kacom.Accounts.ApiKey and allow multiple keys per user. Update the /account UI to list all keys with their labels and individual revoke buttons.

Deliverables

  • Rate limiting plug on :mcp pipeline (60 req/min per user)
  • MCP access log with tool name, user id, duration
  • force_ssl verified in production deploy config
  • label attribute on ApiKey + multi-key support (optional, do if needed)

📋 Task Resource Reference

Kacom.Tasks.Task — lib/kacom/tasks/task.ex
idUUID primary key
titlestring, required, public
descriptionstring, optional, public
status:todo | :in_progress | :done | :archived — default :todo, required, public
priority:high | :medium | :low | :none — default :none, required, public
due_datedate, optional, public
completed_atutc_datetime, set automatically by :complete action, public
user_idbelongs_to User — set by relate_actor(:user) on create, not agent-settable
category_idbelongs_to Category, optional, public
inserted_at, updated_attimestamps

Actions Exposed as MCP Tools

Action Accepts Side Effects
:create title, description, category_id, status, priority, due_date Relates actor as user. Sets completed_at if status is :done on create.
:update title, description, category_id, status, priority, due_date Sets completed_at when status→done (if nil). Clears completed_at when status→todo or in_progress.
:complete (none — id only for record lookup) Sets status = :done, completed_at = now().
:reopen (none — id only for record lookup) Sets status = :todo, clears completed_at.
:archive (none — id only for record lookup) Sets status = :archived.
:destroy (none — id only for record lookup) Permanently deletes the task.

Decisions

  • Tool generation strategy Auto-generated from Ash resource actions via AshAi. No hand-written JSON schemas. Tool schemas update automatically when actions change.
  • Auth method API key in Authorization: Bearer header. Simple, stateless, revocable. One key per user for now; multi-key + labels deferred to Phase 4.
  • User scoping Agents operate as a specific user — their tasks, their categories. No cross-user access possible; enforced at the Ash Policy layer, not application code.
  • Separate status-transition tools Expose complete_task, reopen_task, archive_task as dedicated tools in addition to update_task. update_task also works for status changes but semantic verbs are clearer for agents and correctly invoke the action's built-in side effects.
  • MCP protocol version 2024-11-05 — the version supported by ash_ai ~> 0.4. Upgrade when AshAi adds support for newer protocol versions.
  • Claude Desktop bridge Use mcp-remote npm package as a stdio→HTTP bridge until Claude Desktop supports HTTP MCP servers natively. Claude Code supports HTTP MCP directly.

? Open Questions

  • Actor context in MCP pipeline ✓ Resolved. AshAuthentication.Strategy.ApiKey.Plug calls Ash.PlugHelpers.set_actor(subject) directly on successful auth. No extra plug needed.
  • AshAi tool description quality The auto-generated tool descriptions from AshAi may be too generic for reliable agent tool selection. Audit in Phase 3 and add manual descriptions where needed.
  • SSE / streaming support MCP 2024-11-05 includes an SSE channel for server-sent notifications. AshAi's current router implementation may not support SSE. Investigate if agents that require SSE (e.g., for long-running tool calls) will work with the current setup.