Skip to content

Latest commit

 

History

History
288 lines (206 loc) · 17 KB

File metadata and controls

288 lines (206 loc) · 17 KB

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.
flowchart LR
  response[Tool response]

  response --> content[Markdown content]
  response --> structured[structuredContent JSON]

  content --> markdownExample["## Branches (5)<br/>Markdown table with links<br/>Next-step hints"]
  content --> annotation["Content annotation<br/>audience: assistant"]
  annotation --> llm[Read by the LLM]

  structured --> jsonExample["branches<br/>pagination<br/>next_steps"]
  jsonExample --> ide[Read by IDEs and programmatic clients]

  markdownExample -.->|same hints| jsonExample
Loading

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:

| 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:

💡 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 (15 Jan 2025 10:30 UTC, or 15 Jan 2025 for a value GitLab sends without a time) 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 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 The image block of an upload or a visualization, shown to the user

Every dispatcher finishes a result the same way: the text block carries the annotation the action's declared content kind resolves to (list, detail, mutate or the assistant default), an image block carries ContentUser, and the next-step hints are set on the typed output when its type declares next_steps.

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):

## 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):

{
  "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:

## Merge Request !243: Fix Login Bug

- **Status**: open
- **Author**: [@alice](https://gitlab.example.com/alice)
- **Created**: 15 Mar 2025 10:30 UTC
- **Updated**: 20 Mar 2025 14:15 UTC
- **Source**: feature/fix-login → main
- **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

A result about one object is a card: a heading and a list of - **Label**: value rows. A table is for a collection of objects that share columns, which is what a list response returns. The shape is not per formatter: every card is written by one writer, and a hand-written row fails make check-md-escaping. See the card contract.

Mutation Response

When you ask "Create a new issue titled 'Fix the login page'":

## Issue Created: #42 — Fix the login page

- **IID**: [#42](https://gitlab.example.com/project/-/issues/42)
- **State**: opened
- **Author**: you
- **Created**: 21 Mar 2025 09:00 UTC

---
💡 **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:

## ❓ Branch Not Found

The branch **"nonexistent" in project 42** does not exist or is not accessible with your current permissions.

---
💡 **Next steps:**
- Use action 'branch.list' to list the project's 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 20 "get" handlers across 20 domains (toolutil.NotFoundResult call sites under internal/tools/).

Embedded Resources

A get result can 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.

An action declares the resource it returns on its ActionSpec: EmbeddedResourcePolicy (none, optional or always) says whether to embed, and EmbeddedResource is the canonical gitlab:// URI template, written with the action's own parameter names. The catalog validator refuses a template whose variables the action does not accept, and the dispatchers of all three surfaces expand the template from the call's parameters after a successful result, so a get embeds the same block whether it was reached as an individual tool, through a meta-tool action, or through gitlab_execute_action. Values are escaped the way RFC 6570 simple expansion escapes them, every character outside A-Z a-z 0-9 - . _ ~ percent-encoded, so project_id: "group/project" lands as gitlab://project/group%2Fproject and a scoped label priority::high as priority%3A%3Ahigh, the form the resource templates accept and a resources/read of that URI resolves. The template is expanded from the parameters as the handler read them, after the same normalisation: a project sent URL-encoded (group%2Fproject) embeds the same URI as one sent plainly on every surface, and one sent through a compatibility alias (project_path) does so wherever the surface accepts the alias, which is the dynamic surface and the meta surface under the opaque or compact parameter schema. An individual tool, and a meta tool under the full schema, refuses a property its schema does not declare before the call reaches the handler, so the alias embeds nothing there because it is never served. The twenty-two actions that declare one (TestEmbeddedResource_EveryGetThatHasAResourceDeclaresIt pins the list):

Action Canonical URI
project.get gitlab://project/{project_id}
project.board_get gitlab://project/{project_id}/board/{board_id}
project.label_get gitlab://project/{project_id}/label/{label_id}
project.milestone_get gitlab://project/{project_id}/milestone/{milestone_iid}
group.get gitlab://group/{group_id}
group.group_label_get gitlab://group/{group_id}/label/{label_id}
group.group_milestone_get gitlab://group/{group_id}/milestone/{milestone_iid}
issue.get gitlab://project/{project_id}/issue/{issue_iid}
merge_request.get gitlab://project/{project_id}/mr/{merge_request_iid}
pipeline.get gitlab://project/{project_id}/pipeline/{pipeline_id}
job.get gitlab://project/{project_id}/job/{job_id}
branch.get gitlab://project/{project_id}/branch/{branch_name}
tag.get gitlab://project/{project_id}/tag/{tag_name}
release.get gitlab://project/{project_id}/release/{tag_name}
repository.commit_get gitlab://project/{project_id}/commit/{sha}
environment.get gitlab://project/{project_id}/environment/{environment_id}
environment.deployment_get gitlab://project/{project_id}/deployment/{deployment_id}
feature_flags.feature_flag_get gitlab://project/{project_id}/feature_flag/{name}
access.deploy_key_get gitlab://project/{project_id}/deploy_key/{deploy_key_id}
wiki.get gitlab://project/{project_id}/wiki/{slug}
snippet.get gitlab://snippet/{snippet_id}
snippet.project_get gitlab://project/{project_id}/snippet/{snippet_id}

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. Error and not-found responses do not embed (the entity does not exist), and neither does a call that omitted one of the template's parameters.

This behaviour is enabled by default and can be disabled globally with GITLAB_MCP_EMBEDDED_RESOURCES=false (env var, both transports) 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 <details> 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