Blog

How to Build a Project Management MCP Server: Connecting Jira, Linear, Asana, ClickUp, Trello, and Monday.com to AI Agents

Lukman Nuriakhmetov
Lukman Nuriakhmetov· Founder & CTO
mcpintegrationsarchitecture

If the tracker your team uses doesn't ship an official MCP server, or the official one only exposes read access, you end up building your own. That's a smaller project than it sounds, and a much bigger security problem than it sounds, because the moment an AI agent can call create_issue or delete_task against your real board, you've built a piece of access-control infrastructure, not a script.

This walks through the actual build: mapping six trackers' APIs to MCP tools, where each one's auth model quietly bites you, and the scoping decisions that determine whether an agent can do exactly what it should and nothing else.

What MCP actually gives you

A Model Context Protocol server exposes two kinds of things to an AI client: tools (functions the model can call, like create_issue) and resources (data it can read, like a project's issue list). It runs over one of two transports:

  • stdio, a local subprocess the client launches directly. Fine for a single developer's own machine.
  • Streamable HTTP, a server other people and other agents connect to remotely. This is where authentication becomes your problem, not the tracker's.

Everything below assumes you're eventually shipping the HTTP version, because that's the one your whole team uses, not just you.

Step 1: Map each tracker's API and auth model

Every tracker below has a workable REST or GraphQL API. The auth model is where they differ, and it's the first place a naive integration goes wrong.

TrackerAPI shapeAuthThe gotcha
JiraREST v3API token + email, or OAuth 2.0 (3LO) for multi-tenant appsA personal API token is scoped to everything that account can touch, not one project
LinearGraphQLPersonal API key or OAuth2A personal key inherits the creator's full workspace permissions
AsanaRESTPersonal access token or OAuth2PATs don't expire on their own; a leaked one stays valid until someone remembers to revoke it
ClickUpREST v2Personal token or OAuth2Token scope is the whole workspace by default
TrelloRESTAPI key + token pairThe token is generated per-user against every board they can see, not a specific board
Monday.comGraphQLPersonal API tokenToken carries the user's full role, including admin actions if they have them

The pattern across all six: the tracker's own auth model gives you an all-or-nothing credential. None of them know or care that you only want an agent to touch one project. That boundary is something you have to build, not something you get for free by picking a "read-only" token if the tracker even offers one.

Step 2: Design tool schemas with strict input validation

Resist the urge to expose the tracker's raw API as a single call_api(method, path, body) tool. It's less code, and it's also handing the model a blank check to hit any endpoint the underlying token can reach.

Define narrow, purpose-built tools instead:

server.tool(
  "create_issue",
  {
    projectId: z.string(),
    title: z.string().max(300),
    description: z.string().max(10000).optional(),
    priority: z.enum(["low", "medium", "high"]).optional()
  },
  async ({ projectId, title, description, priority }) => {
    assertProjectAllowed(projectId); // see Step 3
    return trackerClient.createIssue({ projectId, title, description, priority });
  }
);

Every field is typed and bounded. The model can't pass an arbitrary JSON blob through to the tracker's API, and assertProjectAllowed is the hook where access control actually lives, not an afterthought bolted onto the HTTP client.

Step 3: Scope access before you scope features

This is the step teams skip, and it's the one that turns "we gave an agent tracker access" into an incident report.

  • Per-agent tokens, not one shared credential. If three agents share one Jira API token, you cannot tell which one made a given change, and you cannot revoke one without cutting off all three. Issue a separate connection (and, ideally, a separate service account) per agent.
  • An explicit project allowlist, enforced in your server, not assumed from the tracker's token scope. Since the tracker's own token is all-or-nothing, your MCP server needs its own layer that checks projectId against a list before the tracker's client ever sees the request.
  • Separate read tools from write tools, and gate destructive ones behind confirmation. list_issues and delete_issue should not be reachable by the same permission check. A delete_issue tool should require the caller to pass the issue's current version or a confirmation field, so a hallucinated call can't silently destroy something.
function assertProjectAllowed(projectId: string) {
  if (!agentContext.allowedProjects.includes(projectId)) {
    throw new Error(`Agent is not authorized for project ${projectId}`);
  }
}

That one check is the difference between "an agent connected to our tracker" and "an agent connected to the one project it's supposed to touch."

Step 4: Store and rotate tokens like they're production secrets, because they are

  • Never commit a tracker token to a repo or a config file that ends up in one. Pull it from a secret manager at runtime.
  • Rotate it, and build the rotation path before you need it during an incident. A token you can't rotate quickly is a token you can't actually revoke when something goes wrong.
  • Use idempotency keys on every write call. An agent retrying a timed-out create_issue shouldn't create the issue twice; a stable key lets the tracker (or your server) recognize the retry as the same logical operation.

Step 5: Handle rate limits and concurrent writes

Every tracker in the table above enforces its own rate limit and returns a 429 (or a GraphQL-level throttling error) when you exceed it. Build exponential backoff around every write call, not just as a try/catch wrapper, because an agent that retries a failed call in a tight loop will get your server's credentials temporarily blocked by the tracker, not just its own request.

The other failure mode is concurrent writes: an agent and a human editing the same issue at the same time. Where the tracker's API supports a version or updatedAt field, check it before writing and fail the call if it's stale, instead of silently overwriting whichever edit landed last.

Step 6: Deploy it without opening a hole in your own security model

If you're shipping the Streamable HTTP version, the MCP endpoint itself needs its own authentication, independent of the tracker's. An unauthenticated MCP server that happens to hold a valid Jira token is a bigger attack surface than the tracker itself, because anyone who finds the endpoint inherits whatever that token can do.

Where this usually goes wrong, in one list

  • A single shared token used by every agent, with no way to tell which one did what.
  • Tool schemas that pass raw request bodies through, so validation happens nowhere.
  • Project scoping assumed from the tracker's token instead of enforced in your own server.
  • No token rotation path built until the day you need one urgently.
  • No backoff on retries, so a stuck agent takes down its own access by tripping the tracker's rate limiter.
  • A remote MCP endpoint with no auth of its own, reachable by anyone who finds the URL.

Skipping all of this

TAM ships as an MCP server already built around every constraint above: each connected agent gets its own account and its own token, scoped to an explicit allowedTools and allowedProjects list baked into the token itself and enforced server-side, not bolted on in application code you have to maintain. Access is revocable per agent without touching anyone else's connection, PR/CI activity links to the matching ticket automatically instead of you having to build that webhook plumbing yourself, and status changes are built to run on that same evidence, with @tam's facilitator layer rolling out on top of it, rather than a raw write call.

Connecting takes one command: Server URL: Published when the global gateway launches. No connector to build, no token rotation schedule to remember, no rate-limit backoff to write yourself. See the full setup walkthrough here and try it against your own workspace.