asana-local-mcp
Provides read-only tools for interacting with Asana, allowing users to list projects, search tasks, retrieve task details and comments, search comments, and fetch attachment metadata or inline images through the Asana REST API.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@asana-local-mcplist my tasks in the Launch project"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Asana Local MCP
A local, read-only MCP server for Asana. It exposes exactly six tools that query Asana through the public REST API with a personal access token, and it never sends a POST, PUT, PATCH, or DELETE request. The server does not run a cloud service, does not store a cache or index, and does not implement OAuth, webhooks, or account registration.
Contract at a glance
GET-only data plane; no mutation endpoints, no token argument in any tool
Six tools:
list_projects,search_tasks,get_task,list_task_comments,search_comments,get_attachmentToken is read only from the
ASANA_ACCESS_TOKENenvironment variableAttachment access links stay out of task and comment output by default
No local writes: no files, no cache, no index
All error messages are sanitized; tokens, headers, and ephemeral URLs never appear in tool output or logs
Requirements
Python 3.11 or newer
uv (the lockfile is
uv.lock)
python3 --version # 3.11+
uv --version # install uv first if missingRelated MCP server: asana-mcp
Install and sync
Clone the repository and install all dependencies (including the dev
extras used by the test suite):
uv sync --all-extrasRun the full test suite:
uv run pytest -qLocal run options
Unpublished local checkout (this repository)
Launch the server straight from the checkout. In an MCP client config, use
uv --directory pointed at the checkout; do not use uvx for an
unpublished package — uvx asana-local-mcp only works once the package is
published to PyPI or an index:
uv --directory /Users/coffeemug/Programming/asana-local-mcp run asana-local-mcpEditable install (development)
uv sync --all-extras
uv run asana-local-mcpPublished execution (from PyPI or an index)
uvx asana-local-mcpAll entry points resolve the same console script,
asana_local_mcp.server:main, which serves the MCP protocol over stdio.
Authentication
The server reads the access token exclusively from the
ASANA_ACCESS_TOKEN environment variable. Never pass the token as a tool
argument: no tool accepts one, and a token in any MCP client config is a
secret waiting to leak.
Create a personal access token in Asana (Profile > My profile settings > Apps > Personal access tokens). Scope it to
attachments:readand the data-access scope your projects need; the attachment tools requireattachments:read.Add it to the environment of your local MCP client configuration only. Never paste the real value into this repository, a shell history, or a commit. Example
claude_desktop_config.json(replace the placeholder):
{
"mcpServers": {
"asana-local": {
"command": "uv",
"args": ["--directory", "/Users/coffeemug/Programming/asana-local-mcp", "run", "asana-local-mcp"],
"env": {"ASANA_ACCESS_TOKEN": "<local-secret>"}
}
}
}If the token is missing or blank, the server exits with code 2 and logs exactly:
ASANA_ACCESS_TOKEN is missing from MCP configuration.Optional: cap inline image bytes with ASANA_INLINE_IMAGE_MAX_BYTES
(default 2097152, maximum 5242880).
Tools
All six tools are declared read_only. Every response is a structured
envelope:
{
"items": [],
"returned_count": 0,
"truncated": false,
"next_cursor": null,
"scope": {},
"message": null
}returned_count always equals len(items). When truncated is true,
next_cursor or message explains how to continue; pass next_cursor
verbatim into the next call of the same tool.
list_projects
Lists projects whose name contains text.
Parameter | Type | Required | Default | Maximum | Description |
| string | No | — | — | Case-insensitive name substring filter |
| integer | No | 25 | 100 | Maximum projects to return |
| string | No | — | — | Continuation cursor from a prior call |
Input:
{
"text": "roadmap",
"limit": 25,
"cursor": null
}Output items:
{
"items": [
{"gid": "1234567890123", "name": "Roadmap 2026", "permalink_url": "https://app.asana.com/0/0/1234567890123", "archived": false}
],
"returned_count": 1,
"truncated": false,
"next_cursor": null,
"scope": {"searched_project_gids": []},
"message": null
}Projects are scoped to the token's accessible workspaces. list_projects
first enumerates workspaces visible to the token (up to 25, across at most 5
Asana API pages), then queries each workspace's project pages with an
explicit workspace parameter — the current Asana API can require one for
GET /projects. Project GIDs are deduplicated across workspaces. Results may
be truncated across workspace and project pages; continue with
next_cursor to resume.
search_tasks
Searches tasks across up to 25 explicitly selected projects. This is the only way to find tasks: discovery never leaves the selected projects.
Parameter | Type | Required | Default | Maximum | Description |
| string array | Yes | — | 25 | Projects to scan (1–25 GIDs) |
| string | No | — | — | Case-insensitive task-name substring |
| boolean | No | — | — |
|
| string | No | — | — | Exact assignee GID |
| string | No | — | — | ISO-8601 timestamp (e.g. |
| integer | No | 50 | 200 | Maximum tasks to return |
Input:
{
"project_gids": ["1234567890123"],
"text": "launch",
"completed": false,
"assignee_gid": null,
"modified_after": "2026-08-01T00:00:00Z",
"limit": 50
}Output items are deduplicated task summaries with their selected-project memberships; a task in multiple selected projects appears once. If the result set is truncated, retry with a narrower filter or fewer projects.
get_task
Returns one task's detail fields plus normalized attachment metadata.
Parameter | Type | Required | Description |
| string | Yes | Decimal Asana task GID |
Input:
{
"task_gid": "1234567890123"
}Output items carry name, permalink_url, assignee, due_on,
completed, modified_at, html_notes, text_notes, projects,
parent, and attachments (metadata only — see the attachment policy).
list_task_comments
Lists comment stories for one task, newest and oldest per Asana's story order, under strict bounds.
Parameter | Type | Required | Default | Maximum | Description |
| string | Yes | — | — | Decimal Asana task GID |
| integer | No | 100 | 500 | Maximum comments to return |
| string | No | — | — | Continuation cursor from a prior call |
Input:
{
"task_gid": "1234567890123",
"limit": 100,
"cursor": null
}Output items are comments with comment_gid, task_gid, text,
html_text, created_at, created_by, and attachments (metadata
only). Attachment metadata for comments is bounded by a shared per-call
budget of 100 fetches; when that budget runs out, the envelope message
says so. If the API repeats an offset, continuation stops and the envelope
message explains that results may be incomplete.
search_comments
Searches comments across selected projects, task-first.
Parameter | Type | Required | Default | Maximum | Description |
| string array | Yes | — | 25 | Projects to scan (1–25 GIDs) |
| string | No | — | — | Case-insensitive task-name filter applied before any story request |
| string | No | — | — | Case-insensitive comment-text filter |
| integer | No | 50 | 100 | Maximum tasks to scan |
| integer | No | 100 | 500 | Maximum comments to return |
Input:
{
"project_gids": ["1234567890123"],
"task_text": null,
"comment_text": "blocker",
"task_limit": 50,
"comment_limit": 100
}The scope object reports searched_project_gids, candidate_task_count,
and separate task_truncated / comment_truncated flags. task_limit and
comment_limit exhaustion are reported independently in message.
get_attachment
Returns one attachment's safe metadata, optionally with its image rendered inline.
Parameter | Type | Required | Default | Description |
| string | Yes | — | Decimal Asana attachment GID |
| boolean | No |
| Render the image inline when allowed |
Input:
{
"attachment_gid": "1234567890123",
"include_image": false
}Output items carry gid, name, resource_subtype, host, size, and
parent. With include_image: true, a successful render adds an MCP image
content block next to the metadata envelope; any rejection is reported as
truncated: true with a reason in message. include_image: false never
adds an image block.
Attachment policy
Task and comment output carries metadata only. Access links (
download_url,view_url,permanent_url) are omitted from task and comment attachments by default, so ephemeral URLs never leak into model context through browsing.get_attachmentreturns fresh, ephemeral access links only when you explicitly call it. Downloads may expire after a short time; re-callget_attachmentfor a fresh link.permanent_urlis only reachable with a valid Asana session in the same browser, so treat it as reference metadata, not a shared link.view_urlmay not be present for every attachment.Attachment bytes are fetched without the bearer token, with an
Accept: image/*header only.
Image policy
Inline images are opt-in and strictly bounded:
Rendering happens only when
get_attachmentis called withinclude_image: true.Only Asana-hosted attachments (
host: "asana") are candidates.Only allowlisted image MIME types render inline:
image/png,image/jpeg,image/gif,image/webp.Default cap: 2 MiB (
ASANA_INLINE_IMAGE_MAX_BYTES=2097152); hard ceiling: 5 MiB (5242880). Larger images return atruncated: trueenvelope with a reason message instead of bytes.No inline video, PDF, document, or other binary; known subtypes are rejected before any download.
Nothing is ever written to disk: no local files, no cache, no index.
Security and privacy
attachments:readis required for the attachment tools; tokens without that scope get safe authentication errors.GET-only data plane: the client and the packaged entry point contain no POST/PUT/PATCH/DELETE path; the contract tests enforce this by inspecting the client source AST and by recording every request the transport makes.
No OAuth, webhooks, cache, index, or cloud deployment. The server is a stdio process on your machine; it stores no state between calls beyond the stateless cursors you pass back.
No arbitrary HTML URL fetches. Attachment discovery reads only
data-asana-gidattributes from Asana HTML; URLs in HTML are never dereferenced.No token, log, or header leakage. Tool errors, server logs, and structured envelopes are sanitized: bearer tokens become
Bearer [REDACTED]and URLs become[REDACTED_URL]. Logs record only the tool name and exception class..envis excluded from version control (see.gitignore), and.env.exampleships an empty token declaration with no secret.
Error messages
Every failure is returned as an error envelope whose message is safe to
show. The full set:
Condition | Message |
Missing/blank token |
|
Non-decimal GID |
|
Limit out of range |
|
Bad timestamp |
|
Bad continuation cursor |
|
Authentication failure |
|
Missing resource |
|
Rate limit |
|
Invalid response |
|
Transient failure |
|
Attachment bytes |
|
Pagination repeat |
|
Truncated results | Message names the bound and how to continue (for example |
Unexpected error |
|
Narrowing or continuing truncated results: pass the returned
next_cursor verbatim into the same tool with the same arguments to fetch
the next batch; or lower limit / add filters so the request completes
inside its bounds. Cursors are stateless and tied to the exact endpoint,
task, and filters that produced them.
Local smoke test (manual, optional)
Never put a real credential in an automated test or a tracked file. This smoke test uses your local client configuration only.
Start an MCP inspector or client pointed at the server. When run from this checkout, launch it with the local command, not
uvx:npx @modelcontextprotocol/inspector -- uv --directory /Users/coffeemug/Programming/asana-local-mcp run asana-local-mcp, with the token injected through the client'senvblock as shown under Authentication. (uvx asana-local-mcponly applies to a published package.)Call
list_projectswith{"limit": 1}and confirm it returns an accessible project from your workspace.Verify shell output, server logs, and
git status/git grepshow no token, noAuthorizationheader, and no attachment URL anywhere.
Development
uv run ruff check src tests # lint
uv run mypy src # strict type check
uv build # build wheel + sdist into dist/
uv run pytest -q # full suite (no network; mock transport only)The suite includes tests/test_read_only_contract.py, which pins the
read-only contract: no mutation methods in the client, every recorded
request is GET, the README documents exactly the six tools the server
exposes, the tracked files carry no real secrets or signed access URLs,
error adapters strip bearer/header/URL fragments, and the whole suite
passes with ASANA_ACCESS_TOKEN absent. No test makes a live Asana
request.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseBqualityCmaintenancePerform Asana operations from an MCP Client such as Anthropic's Claude Desktop Application, and many more.223,169147MIT
- AlicenseBqualityCmaintenanceMCP server for the Asana API that allows AI agents to read and optionally write to Asana tasks, comments, and custom fields, with tiered access controls and no delete tools.827MIT
- AlicenseNot gradedqualityCmaintenanceEnables to interact with Asana through Claude, with read and write tools for tasks, projects, tags, and custom fields, with no destructive operations.27MIT
- AlicenseCqualityDmaintenanceEnables interaction with Asana through natural language, supporting task and project management via MCP clients like Claude.26MIT
Related MCP Connectors
Read-only MCP server for Muovi, Argentina's trust-first local services marketplace (6 tools).
Gateway between LLM agents and world data through eight tools and a bundled endpoint catalog.
Read-only DERO blockchain MCP: 33 tools (12 composites) incl. TELA discovery + bundled docs.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/minhtrancmvn/asana-local-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server