# Output Format
How gitlab-mcp-server formats tool responses for both human and machine consumption.
> **DiΓ‘taxis type**: Explanation
> **Audience**: π€ End users, AI assistant users
---
## Overview
Successful tool responses contain **two representations** of the same data:
1. **Markdown content** β human-readable text with tables, clickable links, and next-step hints. Targeted at the LLM (`audience: assistant`) so it can reason over the data and present it to you.
2. **Structured JSON** (`structuredContent`) β machine-readable data for programmatic clients. IDEs like VS Code read this to extract fields, and it also includes a `next_steps` array with actionable hints.
```mermaid
flowchart LR
response[Tool response]
response --> content[Markdown content]
response --> structured[structuredContent JSON]
content --> markdownExample["## Branches (5)
Markdown table with links
Next-step hints"]
content --> annotation["Content annotation
audience: assistant"]
annotation --> llm[Read by the LLM]
structured --> jsonExample["branches
pagination
next_steps"]
jsonExample --> ide[Read by IDEs and programmatic clients]
markdownExample -.->|same hints| jsonExample
```
## What You See as a User
When you ask your AI assistant a question like *"Show me the open merge requests"*, the response typically includes:
### Clickable Links
List results include clickable links that open directly in GitLab:
```markdown
| MR | Title | Author | Status |
|----|-------|--------|--------|
| [!243](https://gitlab.example.com/project/-/merge_requests/243) | Fix login | alice | open |
| [!241](https://gitlab.example.com/project/-/merge_requests/241) | Add tests | bob | open |
```
Click on `!243` to open the merge request in your browser. This works for merge requests, issues, pipelines, projects, branches, commits, releases, todos, milestones, members, and other list surfaces that include GitLab web URLs.
### Next Steps
After each response, you will see suggested next actions:
```text
π‘ Next steps:
- Get details of a specific MR by its number
- Create a new merge request
- Approve or merge an open MR
```
These hints are available in both the Markdown and JSON output, so your IDE can display them regardless of which format it reads. Tool execution errors use `isError: true` and may omit `structuredContent` so clients do not confuse an error payload with a successful typed result.
### Formatted Data
- **Dates** appear in readable format (`2025-01-15 10:30`) instead of raw ISO timestamps
- **Status** values use emoji indicators (β
success, β failed, β³ running)
- **Pagination** shows "Page 1 of 3 (20 per page)" with hints to request more
## How Clients Consume Responses
Different MCP clients read different parts of the response:
| Client | Reads Markdown `content` | Reads `structuredContent` JSON | How hints arrive |
| ----------------------- | ------------------------ | ------------------------------ | -------------------------------------------------------- |
| **VS Code / Copilot** | β Ignores | β
Primary | `next_steps` array in JSON |
| **Cursor** | β
Primary | β
Also available | Both `π‘ Next steps` in Markdown and `next_steps` in JSON |
| **Claude Desktop** | β
Primary | β
Also available | Both formats |
| **CLI tools** | β
Primary | β Often ignored | `π‘ Next steps` in Markdown |
| **Custom HTTP clients** | Depends | Depends | Both available in JSON-RPC response |
The server ensures hints appear in **both** formats so no client misses them.
## Content Annotations
Every Markdown response includes [MCP annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/annotations) that tell the client who the content is for and how important it is:
| Annotation | Audience | Priority | Used For |
| ------------------ | ------------------- | -------- | ------------------------------------ |
| `ContentList` | `assistant` | 0.4 | List and search results |
| `ContentDetail` | `assistant` | 0.6 | Single-entity details (get, show) |
| `ContentMutate` | `assistant` | 0.8 | Create, update, delete confirmations |
| `ContentAssistant` | `assistant` | 0.7 | General assistant-targeted content |
| `ContentUser` | `user` | 0.8 | Content for direct user display |
| `ContentBoth` | `user`, `assistant` | 0.5 | Content for both audiences |
### What Does `audience: ["assistant"]` Mean?
When content is marked `audience: ["assistant"]`, it tells the MCP client: *"This content is for the AI to reason over, not for direct display to the user."* This prevents the raw Markdown from being shown alongside the formatted JSON in clients like VS Code that render both. The LLM still sees and uses the Markdown β it just is not duplicated in the UI.
### What Is Priority?
The `priority` value (0.0 to 1.0) hints to the client how important the content is relative to other content in the same response. Higher priority content should be processed first. For example, a mutation result (0.8) is more immediately relevant than a list result (0.4).
## Tool Annotations
Separate from content annotations, every **tool** has behavioral annotations that describe what it does:
| Annotation | Type | Meaning |
| ----------------- | ------- | ---------------------------------------------------------------------------- |
| `readOnlyHint` | `bool` | The tool only reads data, never modifies anything |
| `destructiveHint` | `*bool` | The tool may perform irreversible operations (delete, drop) |
| `idempotentHint` | `bool` | Calling the tool multiple times with the same input produces the same result |
| `openWorldHint` | `*bool` | The tool interacts with external systems (GitLab API) |
These annotations help your AI assistant and IDE make safety decisions:
- Tools with `destructiveHint: true` may trigger confirmation prompts
- Tools with `readOnlyHint: true` can be called freely without risk
- Tools with `idempotentHint: true` are safe to retry on failure
## Response Format Examples
### List Response
When you ask *"Show me the branches in my project"*:
**Markdown content** (what the LLM sees):
```markdown
## Branches (5)
| Branch | Protected | Default | Merged | Web URL |
|--------|-----------|---------|--------|---------|
| [main](https://gitlab.example.com/.../main) | β
| β
| β | [β](url) |
| [develop](https://gitlab.example.com/.../develop) | β
| β | β | [β](url) |
| feature/login | β | β | β | [β](url) |
Page 1 of 1 (20 per page) Β· 5 items
---
π‘ **Next steps:**
- When presenting these results, always include the clickable [text](url) links
- Get details of a specific branch
- Create a new branch from any ref
```
**Structured JSON** (what VS Code reads):
```json
{
"branches": [
{ "name": "main", "protected": true, "default": true, "web_url": "https://..." },
{ "name": "develop", "protected": true, "default": false, "web_url": "https://..." }
],
"pagination": { "page": 1, "per_page": 20, "total_items": 5, "total_pages": 1, "has_more": false },
"next_steps": [
"When presenting these results, always include the clickable [text](url) links",
"Get details of a specific branch",
"Create a new branch from any ref"
]
}
```
### Detail Response
When you ask *"Show me merge request `!243`"*:
**Markdown content**:
```markdown
## Merge Request !243: Fix Login Bug
| Field | Value |
|-------|-------|
| Status | open |
| Author | [alice](https://gitlab.example.com/alice) |
| Created | 2025-03-15 10:30 |
| Updated | 2025-03-20 14:15 |
| Source | feature/fix-login β main |
| Web URL | [!243](https://gitlab.example.com/project/-/merge_requests/243) |
---
π‘ **Next steps:**
- View the changes (diff) for this MR
- List discussions and review comments
- Approve or merge this MR
```
### Mutation Response
When you ask *"Create a new issue titled 'Fix the login page'"*:
```markdown
## Issue Created: #42 β Fix the login page
| Field | Value |
|-------|-------|
| IID | [#42](https://gitlab.example.com/project/-/issues/42) |
| State | opened |
| Author | you |
| Created | 2025-03-21 09:00 |
---
π‘ **Next steps:**
- Add labels or assignees to this issue
- Create a merge request linked to this issue
- Add a comment with more details
```
### Not Found Response
When a "get" operation targets a resource that does not exist the server returns an informational result instead of an opaque error:
```text
## β Branch Not Found
The branch **"nonexistent" in project 42** does not exist or is not accessible with your current permissions.
π‘ **Next steps:**
- Use gitlab_branch_list with project_id to list available branches
- Verify the branch name is spelled correctly (case-sensitive)
```
Not-found responses have `IsError: true` but include actionable hints so the AI assistant can self-correct or suggest alternatives. This pattern covers 27 "get" handlers across 21 domains.
## Embedded Resources
Selected `gitlab_*_get` tools attach an additional content block of type `resource` (`mcp.EmbeddedResource`) carrying the canonical MCP resource URI for the entity returned. This lets clients that only render `Content` blocks (and ignore `StructuredContent`) still surface a stable, dereferenceable identifier the user or LLM can pass to `resources/read`, follow-up tool calls, or UI deep-links.
Currently embedded by 22 `gitlab_*_get` handlers:
| Tool | Canonical URI |
| ---------------------------- | ------------------------------------------------------------ |
| `gitlab_board_get` | `gitlab://project/{project_id}/board/{board_id}` |
| `gitlab_branch_get` | `gitlab://project/{project_id}/branch/{branch_name}` |
| `gitlab_commit_get` | `gitlab://project/{project_id}/commit/{sha}` |
| `gitlab_deploy_key_get` | `gitlab://project/{project_id}/deploy_key/{key_id}` |
| `gitlab_deployment_get` | `gitlab://project/{project_id}/deployment/{deployment_id}` |
| `gitlab_environment_get` | `gitlab://project/{project_id}/environment/{environment_id}` |
| `gitlab_feature_flag_get` | `gitlab://project/{project_id}/feature_flag/{name}` |
| `gitlab_group_get` | `gitlab://group/{group_id}` |
| `gitlab_group_label_get` | `gitlab://group/{group_id}/label/{label_id}` |
| `gitlab_group_milestone_get` | `gitlab://group/{group_id}/milestone/{milestone_iid}` |
| `gitlab_issue_get` | `gitlab://project/{project_id}/issue/{issue_iid}` |
| `gitlab_job_get` | `gitlab://project/{project_id}/job/{job_id}` |
| `gitlab_label_get` | `gitlab://project/{project_id}/label/{label_id}` |
| `gitlab_milestone_get` | `gitlab://project/{project_id}/milestone/{milestone_iid}` |
| `gitlab_mr_get` | `gitlab://project/{project_id}/mr/{merge_request_iid}` |
| `gitlab_pipeline_get` | `gitlab://project/{project_id}/pipeline/{pipeline_id}` |
| `gitlab_project_get` | `gitlab://project/{project_id}` |
| `gitlab_project_snippet_get` | `gitlab://project/{project_id}/snippet/{snippet_id}` |
| `gitlab_release_get` | `gitlab://project/{project_id}/release/{tag_name}` |
| `gitlab_snippet_get` | `gitlab://snippet/{snippet_id}` |
| `gitlab_tag_get` | `gitlab://project/{project_id}/tag/{tag_name}` |
| `gitlab_wiki_get` | `gitlab://project/{project_id}/wiki/{slug}` |
The embedded resource carries `MIMEType: "application/json"` and a `Text` payload equal to the JSON-marshaled output struct β duplicating `StructuredContent` so simpler clients lose nothing. Not-found responses do **not** embed (the entity does not exist).
This behaviour is enabled by default and can be disabled globally with `EMBEDDED_RESOURCES=false` (env var) or `--embedded-resources=false` (HTTP-mode flag) as a kill-switch for clients that don't tolerate duplicate content blocks.
## Per-Route OutputSchema (Meta-Tools)
Meta-tools declare a single tool-level `OutputSchema` (the envelope with `next_steps` and `pagination` fields). In addition, each action route can carry its own output schema describing the exact shape returned by that specific action.
Per-route schemas are populated automatically when using typed route constructors (`RouteAction[T,R]`, `DestructiveAction[T,R]`, `RouteActionWithRequest[T,R]`, `DestructiveActionWithRequest[T,R]`) and the typed void/delete constructors (`RouteVoidAction[T]`, `DestructiveVoidAction[T]`, `DestructiveVoidActionWithRequest[T]`). Plain untyped `Route()` calls do not automatically get per-route schemas.
These schemas are:
- **Exposed in `llms-full.txt`** under "Action Output Schemas" for each meta-tool, using collapsible `` blocks per action
- **Audited by `cmd/audit_surface_quality -view=output`** which reports routes with missing schemas (category `route-output-schema`)
- **Accessible programmatically** via `tools.BuildActionCatalog(...)`; meta-tools, dynamic tools, the `gitlab://tools` manifest, and audits project route maps from that canonical catalog
- **Cached** by `reflect.Type` to avoid redundant schema generation
This enables LLMs to predict the exact response shape of each meta-tool action without trial-and-error.
## See Also
- [Architecture β Response Format](../concepts/architecture.md#design-patterns) β implementation patterns for dual-output
- [Meta-Tools](../concepts/meta-tools.md) β how domain meta-tools use this format
- [Tools Reference](tools/README.md) β per-domain tool documentation
- [Troubleshooting](../guides/troubleshooting.md) β common output format issues
- [MCP Specification β Annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/annotations)