Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3a750fd
docs(guides): note supported Node version range in Quick Start prereq…
Jun 5, 2026
1560467
feat(types): add 'jira' to ChannelSource union
Jun 8, 2026
1a634dd
feat(jira): add DDB table constructs for projects, users, workspaces
Jun 8, 2026
a1223e3
feat(jira): add webhook, processor, link Lambdas + shared helpers
Jun 8, 2026
27c2cbd
feat(jira): add JiraIntegration construct + stack wiring
Jun 8, 2026
7bb0969
feat(jira): wire agent-side MCP + OAuth resolver for jira channel
Jun 8, 2026
37e4ea2
feat(cli): add bgagent jira commands (app-template, setup, link, map)
Jun 8, 2026
9562ab3
test(jira): add webhook, processor, and link handler tests
Jun 8, 2026
ddfa65b
docs(jira): add setup guide, ADR-014, and integration listings
Jun 8, 2026
8393f14
test(jira): close build/coverage gaps for jira integration
Jun 9, 2026
e5cd8ca
fix(jira): resolve cloudId from sole tenant when webhook omits it
Jun 10, 2026
b4e94e1
feat(jira): post issue progress comments via REST shim
Jun 10, 2026
c84cc66
Merge branch 'main' into feat/288-jira-integration
mayakost Jun 10, 2026
1e4a4a0
fix(jira): repair botched merge in test_config.py imports
Jun 10, 2026
0ca9003
fix(jira): type _config base dict so ty accepts TaskConfig(**base)
Jun 10, 2026
ae7e70f
fix(jira): apply ruff format and resync stale docs mirrors
Jun 10, 2026
2dcfb3c
fix(jira): address PR #302 review — security binding, token refresh, …
Jun 10, 2026
0f47343
Merge branch 'main' into feat/288-jira-integration
mayakost Jun 10, 2026
97234f2
fix(docs): repair botched main-merge in sync-starlight.mjs
Jun 10, 2026
9ca9b40
fix(cdk): repair botched main-merge in agent.ts
Jun 10, 2026
cda7db4
fix(cdk): correct DynamoDB table-count assertion + import ordering
Jun 10, 2026
15c559b
Merge remote-tracking branch 'upstream/main' into feat/288-jira-integ…
Jun 10, 2026
9dd66b8
fix(jira): post Lambda-side feedback to api.atlassian.com gateway base
Jun 11, 2026
b1c2322
Merge branch 'main' into feat/288-jira-integration
krokoko Jun 12, 2026
0bf4e59
Merge branch 'main' into feat/288-jira-integration
krokoko Jun 12, 2026
ce96490
Merge branch 'main' into feat/288-jira-integration
krokoko Jun 12, 2026
644238f
Merge branch 'main' into feat/288-jira-integration
krokoko Jun 13, 2026
b448f79
Merge branch 'main' into feat/288-jira-integration
krokoko Jun 15, 2026
fb07245
Merge branch 'main' into feat/288-jira-integration
krokoko Jun 16, 2026
bf5ca80
fix(jira): extract magic numbers into named constants
Jun 16, 2026
67d2886
Merge branch 'main' into feat/288-jira-integration
isadeks Jun 16, 2026
2dd9743
fix(jira): address review — HMAC empty-secret guard, verify tests, do…
Jun 16, 2026
b6ed676
Merge branch 'main' into feat/288-jira-integration
krokoko Jun 16, 2026
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

## What is ABCA

**ABCA (Autonomous Background Coding Agents on AWS)** is a sample of what a self-hosted background coding agents platform might look like on AWS. You submit a coding task (via Slack, Linear, CLI, or webhook), walk away, and come back to a ready-to-review PR. The agent clones the repo, writes code, runs tests, and opens the PR autonomously in an isolated cloud environment. No babysitting, no IDE sessions, no back-and-forth.
**ABCA (Autonomous Background Coding Agents on AWS)** is a sample of what a self-hosted background coding agents platform might look like on AWS. You submit a coding task (via Slack, Linear, Jira, CLI, or webhook), walk away, and come back to a ready-to-review PR. The agent clones the repo, writes code, runs tests, and opens the PR autonomously in an isolated cloud environment. No babysitting, no IDE sessions, no back-and-forth.

## Why it matters

Expand All @@ -31,7 +31,7 @@

## The Use Case

Users submit tasks through webhooks, CLI, Slack,... For each task, the orchestrator executes the blueprint: an isolated environment is provisioned, an agent clones the target GitHub repository, creates a branch, works on the task, and opens a pull request.
Users submit tasks through webhooks, CLI, Slack, Linear, Jira,... For each task, the orchestrator executes the blueprint: an isolated environment is provisioned, an agent clones the target GitHub repository, creates a branch, works on the task, and opens a pull request.

Key characteristics:

Expand Down
97 changes: 76 additions & 21 deletions agent/src/channel_mcp.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,43 @@
"""Channel-specific MCP configuration for the agent container.

For Linear-origin tasks we write (or merge into) ``.mcp.json`` in the cloned
repo ``cwd`` so the Claude Agent SDK — configured with
``setting_sources=["project"]`` — picks up the Linear MCP at session start
and exposes ``mcp__linear-server__*`` tools.
For inbound channel sources that have a hosted MCP we write (or merge into)
``.mcp.json`` in the cloned repo ``cwd`` so the Claude Agent SDK — configured
with ``setting_sources=["project"]`` — picks up the channel MCP at session
start and exposes the server's tools.

Currently wired channels:
- ``linear`` → Linear hosted MCP (``mcp__linear-server__*`` tools)
- ``jira`` → Atlassian Remote MCP (``mcp__jira-server__*`` tools)

For all other channel sources this is a no-op: no MCP is written, and the
SDK sees no Linear tools. That's the gate keeping Slack/API/webhook tasks
from touching Linear.
SDK sees no channel-specific tools.

See: cdk/src/handlers/linear-webhook-processor.ts (inbound), runner.py
(SDK invocation), plans at ~/.claude/plans/linear-mcp-findings.md.
See: cdk/src/handlers/{linear,jira}-webhook-processor.ts (inbound),
runner.py (SDK invocation).
"""

from __future__ import annotations

import json
import os
from typing import Any
from typing import TYPE_CHECKING, Any

from shell import log

if TYPE_CHECKING:
from collections.abc import Callable

# ─── Linear ──────────────────────────────────────────────────────────────────

#: Linear MCP endpoint — hosted by Linear, Streamable HTTP transport.
LINEAR_MCP_URL = "https://mcp.linear.app/mcp"

#: Key name inside ``mcpServers``. Tools surface as
#: ``mcp__linear-server__*`` in the Agent SDK (verified in findings).
#: ``mcp__linear-server__*`` in the Agent SDK.
LINEAR_MCP_SERVER_KEY = "linear-server"

#: Env var name the MCP server entry reads via ``${LINEAR_API_TOKEN}``
#: placeholder expansion. Populated from ``LinearApiTokenSecret`` by run.sh.
#: placeholder expansion. Populated from the OAuth secret by config.py.
LINEAR_API_TOKEN_ENV = "LINEAR_API_TOKEN" # noqa: S105 — env var *name*, not a secret value


Expand All @@ -44,11 +52,55 @@ def _linear_server_entry() -> dict[str, Any]:
}


# ─── Jira (Atlassian Remote MCP) ─────────────────────────────────────────────

#: Atlassian Remote MCP endpoint — Streamable HTTP transport.
#:
#: NOTE: Atlassian's Remote MCP rolled out in mid-2025 and may still be in
#: preview / gated rollout when this code first deploys. Confirm the public
#: URL + auth contract before relying on this in production. If gated, fall
#: back to a REST shim in a future ``jira_reactions.py`` module (Plan B).
JIRA_MCP_URL = "https://mcp.atlassian.com/v1/sse"

#: Key name inside ``mcpServers``. Tools surface as ``mcp__jira-server__*``
#: in the Agent SDK. If this changes the agent prompt's channel addendum
#: must be updated in lockstep.
JIRA_MCP_SERVER_KEY = "jira-server"

#: Env var name the Jira MCP server entry reads via ``${JIRA_API_TOKEN}``
#: placeholder expansion. Populated from the per-tenant OAuth secret by
#: config.resolve_jira_oauth_token.
JIRA_API_TOKEN_ENV = "JIRA_API_TOKEN" # noqa: S105 — env var *name*, not a secret value


def _jira_server_entry() -> dict[str, Any]:
"""Build the `mcpServers` entry for Atlassian's Remote MCP."""
return {
"type": "http",
"url": JIRA_MCP_URL,
"headers": {
"Authorization": f"Bearer ${{{JIRA_API_TOKEN_ENV}}}",
},
}


# ─── Dispatch ────────────────────────────────────────────────────────────────

#: Per-channel ``mcpServers`` entry builder. The channel_source values mirror
#: ``ChannelSource`` in cdk/src/handlers/shared/types.ts. Sources that don't
#: have a hosted MCP (api, webhook, slack) intentionally have no entry here —
#: the gate in ``configure_channel_mcp`` short-circuits on missing keys.
CHANNEL_MCP_BUILDERS: dict[str, tuple[str, Callable[[], dict[str, Any]]]] = {
"linear": (LINEAR_MCP_SERVER_KEY, _linear_server_entry),
"jira": (JIRA_MCP_SERVER_KEY, _jira_server_entry),
}


def _read_existing_mcp_config(path: str) -> dict[str, Any]:
"""Return the parsed .mcp.json at ``path``, or an empty dict if absent/invalid.

Malformed JSON is logged and treated as absent — we prefer to overlay a
valid Linear entry than to crash the agent because a user committed a
valid channel entry than to crash the agent because a user committed a
broken .mcp.json to their repo.
"""
if not os.path.isfile(path):
Expand All @@ -67,23 +119,26 @@ def _read_existing_mcp_config(path: str) -> dict[str, Any]:
def configure_channel_mcp(repo_dir: str, channel_source: str) -> bool:
"""Write or merge a channel-specific ``.mcp.json`` into ``repo_dir``.

Gated on ``channel_source``:
* ``'linear'`` → ensure the ``linear-server`` entry is present in
Looks up ``channel_source`` in :data:`CHANNEL_MCP_BUILDERS`:
* present → ensure the corresponding ``mcpServers`` entry is in
``.mcp.json`` (merges into any existing config without clobbering
other servers). Returns True.
* anything else → no-op. Returns False.
* absent → no-op. Returns False.

Args:
repo_dir: the cloned-repo working directory the SDK will use as ``cwd``.
channel_source: inbound channel (``TaskConfig.channel_source``).

Returns:
True if a Linear MCP entry was (re)written into ``repo_dir/.mcp.json``,
False otherwise (including any non-Linear channel or missing repo_dir).
True if a channel MCP entry was (re)written, False otherwise (channel
unmapped, missing repo_dir, or write failure).
"""
if channel_source != "linear":
builder_entry = CHANNEL_MCP_BUILDERS.get(channel_source)
if builder_entry is None:
return False

server_key, build_entry = builder_entry

if not repo_dir or not os.path.isdir(repo_dir):
log("WARN", f"configure_channel_mcp: repo_dir missing or not a directory: {repo_dir!r}")
return False
Expand All @@ -94,19 +149,19 @@ def configure_channel_mcp(repo_dir: str, channel_source: str) -> bool:
servers = config.get("mcpServers")
if not isinstance(servers, dict):
servers = {}
servers[LINEAR_MCP_SERVER_KEY] = _linear_server_entry()
servers[server_key] = build_entry()
config["mcpServers"] = servers

try:
with open(mcp_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2)
f.write("\n")
except OSError as e:
log("ERROR", f"Failed to write Linear MCP config to {mcp_path}: {e}")
log("ERROR", f"Failed to write {channel_source} MCP config to {mcp_path}: {e}")
return False

log(
"TASK",
f"Linear MCP configured at {mcp_path} (server key: {LINEAR_MCP_SERVER_KEY})",
f"{channel_source} MCP configured at {mcp_path} (server key: {server_key})",
)
return True
Loading
Loading