Skip to main content
Glama
chaunceyplum

workfront-fusion-mcp

by chaunceyplum
README.md
    # Workfront & Workfront Fusion MCP

    One repository, **14 independently deployable Lambda functions** -- one per
    Workfront/Fusion domain -- each exposing only its own scoped set of MCP
    tools at its own HTTP route. **282 tools total**, covering every endpoint
    listed in `WORKFRONT_MCP_SOLUTIONS.md` (full CRUD per object where the
    underlying API supports it, not just the common list/get/create path).
    See `WORKFRONT_MCP_SOLUTIONS.md` for the full
    architecture writeup (endpoint tables, call-order dependencies per server,
    auth details, and the single-repo-vs-separate-repos tradeoff this repo
    settled on).

    This mirrors the pattern already proven in `chaunceyplum/mcp` (the AEC
    Adobe Experience Cloud MCP server): a stateless JSON-RPC 2.0 Lambda
    handler per server, a single flat `TOOLS` dict per function, and
    credentials resolved from SSM at deploy time via `{{resolve:ssm:...}}`.
    The difference here: instead of one Lambda with all tools, **each server
    is its own function with its own route**, so an MCP client only ever sees
    the tools relevant to whichever endpoint(s) it's wired to.

    ## Why one repo instead of separate repos per server

    We evaluated splitting each server into its own GitHub repo (see the
    "one repo per server" alternative in `WORKFRONT_MCP_SOLUTIONS.md`) and
    settled on a single repo with N independently deployable functions
    instead, because:

    - **The actual goal -- "the LLM only sees relevant tools" -- is a runtime
      endpoint property, not a source-control property.** A client hitting
      `/mcp/workfront/core` never sees another server's
      tools regardless of which repo the code lives in.
    - **Isolation that matters (auth, blast radius) comes from per-function
      config, not per-repo config**, and SAM gives us that directly: every
      function below has its **own** `Environment` block resolving its
      **own** SSM namespace -- nothing is shared via `Globals`, so a bug in
      one server's code can't read another server's credentials.
    - **One CI pipeline, one set of shared tooling (`common/`)** is simpler
      to maintain solo/small-team than 14 repos + a versioned shared package,
      and repos are cheap to split out later if team ownership ever actually
      diverges.

    ## Servers

    | Folder | Route | Covers |
    |---|---|---|
    | Folder | Route | Tools | Covers |
    |---|---|---|---|
    | `servers/core/` | /mcp/workfront/core | 43 | Core Work Management: portfolios, programs, templates, template tasks, projects, tasks, and issues (incl. issue→task conversion). |
| `servers/users/` | /mcp/workfront/users | 36 | User & Resource Management: companies, roles, users, teams, resource pools, and resource allocations. |
| `servers/documents/` | /mcp/workfront/documents | 27 | Document Management: folders, documents, versions, approvals, and document webhook subscriptions. |
| `servers/time_approval/` | /mcp/workfront/time-approval | 25 | Time & Approval: approval paths, timesheets (incl. submit), hour entries, and approval instances. |
| `servers/metadata/` | /mcp/workfront/metadata | 26 | Custom Fields & Metadata: parameters, parameter groups, categories (custom forms, incl. field-attach and category-assign), and category-parameter mappings. |
| `servers/search/` | /mcp/workfront/search | 5 | Search & Query: object-scoped search, generic search, named queries, and saved report execution. |
| `servers/comments/` | /mcp/workfront/comments | 11 | Commenting & Collaboration (Comment Stream API v1): full CRUD on comments, replies, and reactions. |
| `servers/planning/` | /mcp/workfront/planning | 24 | Planning API (v2): full CRUD on workspaces, record types, fields, views, and bulk records. |
| `servers/misc/` | /mcp/workfront/misc | 48 | Miscellaneous Objects: notes, messages, reports, calendars, preferences, config, activity updates, and journal entries. |
| `servers/fusion_org/` | /mcp/fusion/org | 11 | Fusion Organization & Team: organizations (read), teams (full CRUD), and user-team assignment. |
| `servers/fusion_connections/` | /mcp/fusion/connections | 6 | Fusion Connection Management: app connections (full CRUD) and connection-specs. |
| `servers/fusion_hooks/` | /mcp/fusion/hooks | 7 | Fusion Webhook & Trigger: webhooks (full CRUD), payload history, trigger definitions. |
| `servers/fusion_scenarios/` | /mcp/fusion/scenarios | 9 | Fusion Scenario Management: scenarios, metadata updates, drafts, templates, manual execution. |
| `servers/fusion_executions/` | /mcp/fusion/executions | 4 | Fusion Execution & Monitoring: execution history, logs, run history. |

    ## Shared code (`common/`)

    | Module | Purpose |
    |---|---|
    | `common/auth.py` | Generic Adobe IMS OAuth2 Server-to-Server (`client_credentials`) token fetch, cached per `(client_id, scope)`. |
    | `common/workfront_client.py` | Classic Workfront REST API client (`/attask/api/v{version}`) + generic per-objcode CRUD helpers + `wf_set_custom_fields` (the `DE:` custom-form-value mechanism). |
    | `common/crud_factory.py` | Builds a standard list/get/create/update/delete/set-custom-fields tool set for a single object code -- most server tool modules are a one-line call into this. |
    | `common/comment_stream_client.py` | Comment Stream API v1 client (separate host + IMS scope from classic REST). |
    | `common/fusion_client.py` | Workfront Fusion API client (separate host + IMS scope; Fusion is experimental per Adobe's own labeling). |
    | `common/planning_client.py` | Planning API v2 client (workspaces / record-types / fields / views / records). |
    | `common/dispatcher.py` | MCP JSON-RPC 2.0 Lambda handler factory (`initialize`, `tools/list`, `tools/call`) -- every server's `lambda_handler.py` is a few lines wiring its own `TOOLS` dict into this. |

    ## Deploy

    ```bash
    sam build
    sam deploy --guided   # first time -- saves samconfig.toml (gitignored; see samconfig.toml.example)
    sam deploy             # subsequent deploys -- updates all 14 functions in one stack
    ```

    `sam deploy` updates the whole stack in one pass; you're still deploying
    14 independent functions with independent env/config, just via one
    command. If you later want to deploy a single server's fix without
    touching the others, target it directly:

    ```bash
    sam deploy --resolve-s3   # or use `aws lambda update-function-code` for a hotfix to one function
    ```

    ### Populate SSM parameters

    Each server reads only its own namespace. Populate the ones for the
    servers you're actually standing up first (see the build order in
    `WORKFRONT_MCP_SOLUTIONS.md` -- Users, then Core, tend to unblock the rest):

    | SSM namespace | Env vars it feeds |
    |---|---|
    | `wf-mcp/core` | `WF_CLIENT_ID`, `WF_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/users` | `WF_CLIENT_ID`, `WF_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/documents` | `WF_CLIENT_ID`, `WF_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/time-approval` | `WF_CLIENT_ID`, `WF_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/metadata` | `WF_CLIENT_ID`, `WF_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/search` | `WF_CLIENT_ID`, `WF_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/comments` | `WF_COMMENTS_CLIENT_ID`, `WF_COMMENTS_CLIENT_SECRET`, `WF_COMMENTS_ORG_ID` |
| `wf-mcp/planning` | `WF_PLANNING_CLIENT_ID`, `WF_PLANNING_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/misc` | `WF_CLIENT_ID`, `WF_CLIENT_SECRET`, `WF_DOMAIN` |
| `wf-mcp/fusion-org` | `FUSION_CLIENT_ID`, `FUSION_CLIENT_SECRET`, `FUSION_API_BASE` |
| `wf-mcp/fusion-connections` | `FUSION_CLIENT_ID`, `FUSION_CLIENT_SECRET`, `FUSION_API_BASE` |
| `wf-mcp/fusion-hooks` | `FUSION_CLIENT_ID`, `FUSION_CLIENT_SECRET`, `FUSION_API_BASE` |
| `wf-mcp/fusion-scenarios` | `FUSION_CLIENT_ID`, `FUSION_CLIENT_SECRET`, `FUSION_API_BASE` |
| `wf-mcp/fusion-executions` | `FUSION_CLIENT_ID`, `FUSION_CLIENT_SECRET`, `FUSION_API_BASE` |

    Example for the core server:

    ```bash
    aws ssm put-parameter --name /wf-mcp/core/client-id     --type SecureString --value "..."
    aws ssm put-parameter --name /wf-mcp/core/client-secret --type SecureString --value "..."
    aws ssm put-parameter --name /wf-mcp/core/domain        --type String       --value "yourtenant"
    ```

    ### Wire up an MCP client

    Each server gets its own entry, pointing at its own route from the SAM
    outputs:

    ```json
    {
      "mcpServers": {
        "workfront-core": { "url": "https://<api-id>.execute-api.<region>.amazonaws.com/mcp/workfront/core" },
        "workfront-users": { "url": "https://<api-id>.execute-api.<region>.amazonaws.com/mcp/workfront/users" }
      }
    }
    ```

    Only wire up the servers relevant to a given client/session -- that's
    what actually keeps the model's tool list scoped.

    ## Status

    Every endpoint listed in `WORKFRONT_MCP_SOLUTIONS.md` has at least one
    corresponding tool (282 total), and every server's `lambda_handler.py`
    was re-verified end-to-end (import + a live `tools/list`/`tools/call`
    dispatch against mocked HTTP) after each round of additions -- see the
    commit history for the specific tool-by-tool audit.

    That said, this is still a **scaffold** in one sense: the JSON-RPC
    dispatcher, auth clients, and CRUD tool wiring are real and complete, but
    several endpoint details are flagged inline (`# NOTE` / docstrings) as
    pending verification against a live tenant -- there is no Workfront/Fusion
    tenant credentialed against this repo yet. Before relying on any tool in
    production, confirm against your own Adobe Developer Console app
    registration:

    - The exact OAuth scope string(s) for classic Workfront REST, Comment
      Stream, Fusion, and Planning (placeholders are marked in `common/*_client.py`).
    - The issue→task conversion endpoint/verb (`servers/core/tools/issues.py`).
    - The Planning API v2 base path (`common/planning_client.py`).
    - The Fusion API base host (`FUSION_API_BASE`).
    - The Document Webhooks API registration path/payload shape
      (`servers/documents/tools/document_webhooks.py` guesses `/documentwebhook`).
    - The named-queries path (`servers/search/tools/named_queries.py` guesses
      `/namedqueries` and `/namedqueries/run`).
    - Whether Fusion actually exposes `PATCH /scenarios/{id}` for metadata
      separately from `/scenarios/{id}/draft` for blueprint edits
      (`servers/fusion_scenarios/tools/scenarios.py`).