Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ LANGFUSE_TRACE_METADATA=ticket_key,ticket_type,project_id,workflow_step,repo,pr_
# joins the Langfuse compose network.
# - The ClickHouse service is then reachable as clickhouse:9000.
GRAFANA_PORT=3010
GRAFANA_BASE_URL=http://localhost:3010
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=grafana
LANGFUSE_DOCKER_NETWORK=langfuse_default
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ Forge agents can access external tools via MCP (Model Context Protocol):

Configure in `mcp-servers.json`. By default, MCP tools are read-only.

Forge also provides a separate read-only session-inspection API and optional MCP
server for users who want to inspect workflow progress without Redis, Langfuse,
or Grafana credentials. See [Session Inspection](docs/reference/session-inspection.md).

## Project Structure

```
Expand Down
1 change: 1 addition & 0 deletions docs/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ LANGFUSE_TRACE_METADATA=ticket_key,ticket_type,project_id,workflow_step,repo,pr_

# Grafana dashboard stack
GRAFANA_PORT=3010
GRAFANA_BASE_URL=http://localhost:3010
LANGFUSE_DOCKER_NETWORK=langfuse_default
CLICKHOUSE_HOST=clickhouse
CLICKHOUSE_PORT=9000
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ graph TD
- [Feature Workflow](guide/feature-workflow.md) — How features flow through Forge
- [Developer Guide](developer-guide.md) — Full local development reference
- [Skills System](skills/index.md) — Customize Forge for your stack
- [Session Inspection](reference/session-inspection.md) — Check workflow progress safely
- [Contributing](dev/contributing.md) — How to contribute

## Key Features
Expand Down
67 changes: 67 additions & 0 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,73 @@ Exposes Prometheus-format metrics for the API server.

Worker metrics are available separately at `http://localhost:8001/metrics`.

---

### Session Summary

```http
GET /api/v1/sessions/{ticket_key}/summary
```

Returns a safe, read-only summary of a Forge workflow session for a Jira ticket.
This endpoint is intended for users who want to inspect session progress without
direct Redis, Langfuse, or Grafana access.

The response is curated and intentionally excludes raw prompts, model messages,
generated artifacts, tool inputs, and full trace metadata.

**Query parameters:**

| Parameter | Default | Description |
|-----------|---------|-------------|
| `logs_limit` | `0` | Optional number of Redis log entries to include. Allowed range: `0` to `50`. Keep this at `0` for the safest user-facing summary. |

**Example:**

```bash
curl http://localhost:8000/api/v1/sessions/AISOS-123/summary
```

**Response:**

```json
{
"summary": {
"ticket_key": "AISOS-123",
"found": true,
"current_node": "implement_task",
"status": "running",
"is_paused": false,
"is_blocked": false,
"retry_count": 0,
"last_error": null,
"ticket_type": "Feature",
"repository": "org/repo",
"pr_number": 42,
"pr_url": "https://github.com/org/repo/pull/42",
"ci_status": "pending",
"artifacts_present": {
"prd": true,
"spec": true,
"rca": false,
"plan": true,
"epics": true,
"tasks": true,
"qa_history": false
},
"observability_links": {
"grafana_issue_detail": "http://localhost:3010/d/forge-issue-detail/forge-issue-detail?orgId=1&var-jira_issue=AISOS-123"
},
"raw_state_exposed": false
},
"notes": [
"This summary is read-only and excludes raw prompts, model messages, generated artifacts, and tool inputs."
]
}
```

Returns `404` if Forge has no persisted session state for the ticket.

## Webhook Configuration

### Jira
Expand Down
8 changes: 7 additions & 1 deletion docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ These variables are used by `docker-compose.yml`, `devtools/docker-compose.dev.y
| Variable | Description |
|----------|-------------|
| `GRAFANA_PORT` | Host port for Grafana (default: `3010`) |
| `GRAFANA_BASE_URL` | Public/base URL for Grafana, used by Forge session summaries and MCP responses to include dashboard links |
| `GRAFANA_ADMIN_USER` | Grafana admin user (default: `admin`) |
| `GRAFANA_ADMIN_PASSWORD` | Grafana admin password (default: `grafana`) |
| `LANGFUSE_DOCKER_NETWORK` | External Docker/Podman network for self-hosted Langfuse when using `devtools/grafana/compose.langfuse-network.yml` (default: `langfuse_default`) |
Expand All @@ -127,4 +128,9 @@ These variables are used by `docker-compose.yml`, `devtools/docker-compose.dev.y

### MCP Servers

MCP server configuration lives in `mcp-servers.json`, not `.env`. See the [MCP servers section](https://github.com/forge-sdlc/forge/blob/main/mcp-servers.json) of the repository.
MCP server configuration for Forge agents lives in `mcp-servers.json`, not `.env`.
The checked-in file is loaded by Forge agents and should only include external
tooling those agents need.

The user-facing Forge session MCP server is configured separately in the user's
assistant client. See [Session Inspection](session-inspection.md).
79 changes: 79 additions & 0 deletions docs/reference/session-inspection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Session Inspection

Forge exposes a safe session-inspection surface so users can understand what is
happening in their workflow without needing Redis, Langfuse, or Grafana
credentials.

The session summary is deliberately curated. It does not expose raw prompts,
model messages, generated artifacts, tool inputs, or full trace metadata.

## HTTP API

Use the Forge API when you want a direct integration or a quick command-line
check:

```bash
curl http://localhost:8000/api/v1/sessions/AISOS-123/summary
```

The endpoint returns workflow progress, current status, PR information, CI
status, artifact presence, and observability links when configured.

See the [API reference](api.md#session-summary) for the full response shape.

## Optional Claude MCP Setup

Forge also ships a read-only stdio MCP server named `forge-session-mcp`. This is
for user assistants such as Claude Desktop or Claude Code. It should be added to
the user's assistant configuration, not to Forge's `mcp-servers.json`.

Do not add `forge-session-mcp` to the checked-in `mcp-servers.json`: that file is
loaded by Forge agents themselves, and the session-inspection server is meant for
external user inspection.

### Claude Code

From the Forge repository:

```bash
uv sync
claude mcp add-json "forge-session" \
'{"type":"stdio","command":"uv","args":["run","forge-session-mcp"],"cwd":"'"$(pwd)"'"}'
```

Then ask Claude for a session summary by Jira ticket key, for example:

```text
Show me the Forge session summary for AISOS-123.
```

### Manual MCP JSON

If your MCP client accepts JSON configuration, add this server entry and adjust
`cwd` to the local Forge repository path:

```json
{
"forge-session": {
"type": "stdio",
"command": "uv",
"args": ["run", "forge-session-mcp"],
"cwd": "/path/to/forge"
}
}
```

The server reads the same `.env` configuration as Forge, so it must be run from a
working Forge checkout with access to the configured Redis/checkpoint backend.

## Available MCP Capability

The MCP server provides:

| Capability | Description |
|------------|-------------|
| `get_session_summary` | Tool that returns a safe summary for a Jira ticket key |
| `forge://sessions/{ticket_key}` | Resource URI that returns the same summary as JSON |

The MCP response includes `raw_state_exposed: false` to make the redaction
contract explicit.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dev = [
[project.scripts]
forge = "forge.cli:main"
forge-serve = "forge.main:main"
forge-session-mcp = "forge.mcp.session:main"

[tool.hatch.build.targets.wheel]
packages = ["src/forge"]
Expand Down
2 changes: 2 additions & 0 deletions src/forge/api/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from forge.api.routes.health import router as health_router
from forge.api.routes.jira import router as jira_router
from forge.api.routes.metrics import router as metrics_router
from forge.api.routes.sessions import router as sessions_router

__all__ = [
"github_router",
"health_router",
"jira_router",
"metrics_router",
"sessions_router",
]
42 changes: 42 additions & 0 deletions src/forge/api/routes/sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Read-only session inspection endpoints."""

from fastapi import APIRouter, HTTPException, Query, status

from forge.sessions.models import SessionSummaryPayload
from forge.sessions.summary import SessionNotFoundError, get_session_summary

router = APIRouter(prefix="/api/v1/sessions", tags=["sessions"])


@router.get(
"/{ticket_key}/summary",
response_model=SessionSummaryPayload,
responses={
200: {"description": "Safe session summary"},
404: {"description": "Session not found"},
},
)
async def session_summary(
ticket_key: str,
logs_limit: int = Query(
default=0,
ge=0,
le=50,
description=(
"Optional number of Redis log entries to include. Defaults to 0 so "
"the public endpoint exposes checkpoint-derived summary only."
),
),
) -> SessionSummaryPayload:
"""Return a safe read-only summary for a Forge session.

This endpoint lets users inspect their session through Forge API without
direct Redis, Langfuse, or Grafana credentials.
"""
try:
return await get_session_summary(ticket_key, logs_limit=logs_limit)
except SessionNotFoundError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"No Forge session found for {ticket_key.strip().upper()}",
)
4 changes: 4 additions & 0 deletions src/forge/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ def detect_model_provider(model_name: str) -> str:
default="",
description="Comma-separated list of TracingField names to include as Langfuse trace metadata",
)
grafana_base_url: str = Field(
default="",
description="Base URL for Forge Grafana dashboards, used for session observability links",
)

# Claude Agent SDK Configuration
agent_enable_tools: bool = Field(
Expand Down
13 changes: 12 additions & 1 deletion src/forge/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@

from forge import __version__
from forge.api.middleware.correlation import CorrelationIdMiddleware
from forge.api.routes import github_router, health_router, jira_router, metrics_router
from forge.api.routes import (
github_router,
health_router,
jira_router,
metrics_router,
sessions_router,
)
from forge.config import get_settings
from forge.observability.config import configure_tracing, shutdown_tracing
from forge.orchestrator.checkpointer import close_redis_pool
Expand Down Expand Up @@ -105,6 +111,10 @@ def create_app() -> FastAPI:
"name": "github",
"description": "GitHub webhook endpoints",
},
{
"name": "sessions",
"description": "Read-only Forge session inspection endpoints",
},
],
docs_url="/docs",
redoc_url="/redoc",
Expand All @@ -128,6 +138,7 @@ def create_app() -> FastAPI:
app.include_router(metrics_router)
app.include_router(jira_router)
app.include_router(github_router)
app.include_router(sessions_router)

return app

Expand Down
1 change: 1 addition & 0 deletions src/forge/mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""MCP servers provided by Forge."""
67 changes: 67 additions & 0 deletions src/forge/mcp/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Read-only MCP server for Forge session inspection."""

from __future__ import annotations

import json
import logging

from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP

from forge.sessions.summary import SessionNotFoundError, get_session_summary

logger = logging.getLogger(__name__)


def create_server() -> FastMCP:
"""Create the Forge session MCP server."""
mcp = FastMCP(
"Forge Session",
instructions=(
"Read-only Forge session inspection. This server exposes curated "
"workflow summaries and intentionally omits raw prompts, model "
"messages, generated artifacts, and tool inputs."
),
)

@mcp.tool(
name="get_session_summary",
description="Return a safe read-only summary for a Forge session by Jira ticket key.",
)
async def get_session_summary_tool(ticket_key: str) -> dict:
try:
payload = await get_session_summary(ticket_key, logs_limit=0)
return payload.as_dict()
except SessionNotFoundError as exc:
return {
"summary": {
"ticket_key": ticket_key.strip().upper(),
"found": False,
"status": "not_found",
"raw_state_exposed": False,
},
"notes": [str(exc)],
}

@mcp.resource(
"forge://sessions/{ticket_key}",
name="Forge Session Summary",
description="Safe JSON summary for a Forge session.",
mime_type="application/json",
)
async def session_summary_resource(ticket_key: str) -> str:
result = await get_session_summary_tool(ticket_key)
return json.dumps(result, indent=2, sort_keys=True)

return mcp


def main() -> None:
"""Run the Forge session MCP server over stdio."""
load_dotenv()
logging.basicConfig(level=logging.INFO)
create_server().run("stdio")


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions src/forge/sessions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Read-only session inspection helpers."""

from forge.sessions.summary import SessionNotFoundError, build_session_summary, get_session_summary

__all__ = ["SessionNotFoundError", "build_session_summary", "get_session_summary"]
Loading
Loading