Documentation
¶
Overview ¶
Package toolutil provides shared utilities for MCP tool handler sub-packages. It contains error handling, pagination, logging, text processing, markdown formatting helpers, and meta-tool infrastructure used across all domain sub-packages under internal/tools/.
This package must never import from domain sub-packages to prevent circular dependencies. The dependency direction is: domain sub-packages → toolutil.
Package toolutil documents and enforces the standard MCP tool output pattern.
All tool handlers in this project follow the triple-return pattern:
func handler(ctx, req, input T) (*mcp.CallToolResult, OutputType, error)
On success:
return ToolResultWithMarkdown(FormatXMarkdown(out)), out, nil
On error:
return nil, ZeroValue, err
The *mcp.CallToolResult provides human-readable Markdown via TextContent, while the OutputType struct provides structured JSON data for programmatic consumption by the SDK serializer.
Meta-tools use MakeMetaHandler with a FormatResultFunc that dispatches to the appropriate sub-package Markdown formatter via the type-switch in markdownForResult (internal/tools/markdown.go).
Index ¶
- Constants
- Variables
- func AccessLevelDescription(level gl.AccessLevelValue) string
- func AdjustPagination(p *PaginationOutput, itemCount int)
- func AppendResourceLink(_ *mcp.CallToolResult, _, _, _ string)deprecated
- func BoolEmoji(v bool) string
- func BoolPtr(b bool) *bool
- func BuildTargetURL(projectWebURL, targetType string, targetIID int64) string
- func CancelledResult(message string) *mcp.CallToolResult
- func ClampPollInterval(v int) int
- func ClampPollTimeout(v int) int
- func ClassifyError(err error) string
- func ClassifyHTTPStatus(code int) string
- func ComputeSHA256(path string) (string, error)
- func ComputeSHA256Reader(r io.Reader) (string, error)
- func ConfirmAction(ctx context.Context, req *mcp.CallToolRequest, message string) *mcp.CallToolResult
- func ConfirmDestructiveAction(ctx context.Context, req *mcp.CallToolRequest, params map[string]any, ...) *mcp.CallToolResult
- func ContainsAny(err error, substrs ...string) bool
- func ContextWithRequest(ctx context.Context, req *mcp.CallToolRequest) context.Context
- func DetectRichContent(body string) string
- func ErrFieldRequired(field string) error
- func ErrInvalidEnum(field, value string, validValues []string) error
- func ErrRequiredInt64(operation, field string) error
- func ErrRequiredString(operation, field string) error
- func ErrorResult(message string) *mcp.CallToolResult
- func ErrorResultMarkdown(domain, action string, err error) *mcp.CallToolResult
- func EscapeMdHeading(s string) string
- func EscapeMdTableCell(s string) string
- func ExtractGitLabMessage(err error) string
- func ExtractHints(md string) []string
- func FormatGID(typeName string, id int64) string
- func FormatGraphQLPagination(p GraphQLPaginationOutput, shown int) string
- func FormatPagination(p PaginationOutput) string
- func FormatTarget(targetType string, targetIID int64, targetTitle, targetURL string) string
- func FormatTime(s string) string
- func IdentityToContext(ctx context.Context, id UserIdentity) context.Context
- func ImageMIMEType(filename string) string
- func IsBinaryFile(filename string) bool
- func IsDestructiveAction(action string) bool
- func IsHTTPStatus(err error, code int) bool
- func IsImageFile(filename string) bool
- func IsNotFound(err error) bool
- func IsTerminalStatus(status string) bool
- func IsYOLOMode() bool
- func IssueStateEmoji(state string) string
- func LogToolCall(tool string, start time.Time, err error)
- func LogToolCallAll(ctx context.Context, req *mcp.CallToolRequest, tool string, start time.Time, ...)
- func MRStateEmoji(state string) string
- func MakeMetaHandler(toolName string, routes map[string]ActionFunc, formatResult FormatResultFunc) ...
- func MarkdownForResult(result any) *mcp.CallToolResult
- func MdTitleLink(title, url string) string
- func MergeVariables(sources ...map[string]any) map[string]any
- func MetaToolSchema(routes map[string]ActionFunc) map[string]any
- func NormalizeText(s string) string
- func NotFoundResult(resource, identifier string, hints ...string) *mcp.CallToolResult
- func OpenAndValidateFile(path string, maxSize int64) (*os.File, os.FileInfo, error)
- func ParseGID(gid string) (typeName string, id int64, err error)
- func ParseOptionalTime(s string) *time.Time
- func PipelineStatusEmoji(status string) string
- func PopulateHints(result *mcp.CallToolResult, setter HintSetter)
- func ProgressReportInterval(total int64) int64
- func RegisterMarkdown[T any](fn func(T) string)
- func RegisterMarkdownResult[T any](fn func(T) *mcp.CallToolResult)
- func RegisteredMarkdownTypeNames() []string
- func RequestFromContext(ctx context.Context) *mcp.CallToolRequest
- func RichContentHint(features, webURL string) string
- func SetUploadConfig(maxFileSize int64)
- func SuccessResult(markdown string) *mcp.CallToolResult
- func TitleFromName(name string) string
- func ToolResultAnnotated(md string, ann *mcp.Annotations) *mcp.CallToolResult
- func ToolResultWithImage(md string, ann *mcp.Annotations, imageData []byte, mimeType string) *mcp.CallToolResult
- func ToolResultWithMarkdown(md string) *mcp.CallToolResult
- func UnmarshalParams[T any](params map[string]any) (T, error)
- func ValidActionsString(routes map[string]ActionFunc) string
- func ValidateDiffPosition(diffLines []DiffLine, newLine, oldLine int) error
- func ValidatePackageFileName(filename string) error
- func ValidatePackageName(name string) error
- func WithHints[O any](result *mcp.CallToolResult, out O, err error) (*mcp.CallToolResult, O, error)
- func WrapErr(operation string, err error) error
- func WrapErrWithHint(operation string, err error, hint string) error
- func WrapErrWithMessage(operation string, err error) error
- func WrapGFMBody(body string) string
- func WriteEmpty(b *strings.Builder, resource string)
- func WriteHints(b *strings.Builder, hints ...string)
- func WriteListSummary(b *strings.Builder, shown int, p PaginationOutput)
- func WritePagination(b *strings.Builder, p PaginationOutput)
- type ActionFunc
- type DeleteOutput
- type DetailedError
- type DiffLine
- type DiffOutput
- type FormatResultFunc
- type GraphQLPageInfo
- type GraphQLPaginationInput
- type GraphQLPaginationOutput
- type GraphQLRawPageInfo
- type HintSetter
- type HintableOutput
- type LineType
- type MetaToolInput
- type PaginationInput
- type PaginationOutput
- type ProgressReader
- type ProgressWriter
- type StringOrInt
- type ToolError
- type UploadConfig
- type UserIdentity
- type VoidOutput
Constants ¶
const ( GraphQLDefaultFirst = 20 GraphQLMaxFirst = 100 )
GraphQL pagination defaults.
const ( DateFormatISO = "2006-01-02" DateTimeFormat = "2006-01-02T15:04:05Z" )
Common date format constants used across tool sub-packages.
const ( FmtMdID = "- **ID**: %d\n" FmtMdName = "- **Name**: %s\n" FmtMdTitle = "- **Title**: %s\n" FmtMdState = "- **State**: %s\n" FmtMdStatus = "- **Status**: %s\n" FmtMdDescription = "- **Description**: %s\n" FmtMdPath = "- **Path**: %s\n" FmtMdVisibility = "- **Visibility**: %s\n" FmtMdEmail = "- **Email**: %s\n" FmtMdUsername = "- **Username**: %s\n" FmtMdTarget = "- **Target**: %s\n" FmtMdCreated = "- **Created**: %s\n" FmtMdUpdated = "- **Updated**: %s\n" FmtMdURL = "- **URL**: [%[1]s](%[1]s)\n" FmtMdURLNewline = "\n- **URL**: [%[1]s](%[1]s)\n" FmtMdAuthorAt = "- **Author**: @%s\n" FmtMdAuthor = "- **Author**: %s\n" FmtMdSectionText = "\n%s\n" TblSep1Col = "| --- |\n" TblSep2Col = "| --- | --- |\n" TblSep3Col = "| --- | --- | --- |\n" TblSep4Col = "| --- | --- | --- | --- |\n" TblSep5Col = "| --- | --- | --- | --- | --- |\n" TblSep6Col = "| --- | --- | --- | --- | --- | --- |\n" TblSep7Col = "| --- | --- | --- | --- | --- | --- | --- |\n" TblSep8Col = "| --- | --- | --- | --- | --- | --- | --- | --- |\n" TblSep9Col = "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" TblSep10Col = "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" FmtRow1Str = "| %s |\n" FmtRow2Str = "| %s | %s |\n" FmtRow3Str = "| %s | %s | %s |\n" FmtRow4Str = "| %s | %s | %s | %s |\n" FmtRow5Str = "| %s | %s | %s | %s | %s |\n" FmtRow6Str = "| %s | %s | %s | %s | %s | %s |\n" FmtRow7Str = "| %s | %s | %s | %s | %s | %s | %s |\n" FmtRow8Str = "| %s | %s | %s | %s | %s | %s | %s | %s |\n" FmtRow9Str = "| %s | %s | %s | %s | %s | %s | %s | %s | %s |\n" FmtRow10Str = "| %s | %s | %s | %s | %s | %s | %s | %s | %s | %s |\n" )
Markdown format constants for repeated table separators and field patterns.
const ( EmojiDraft = "\U0001F4DD" // 📝 EmojiWarning = "\u26A0\uFE0F" // ⚠️ EmojiConfidential = "\U0001F512" // 🔒 EmojiArchived = "\U0001F4E6" // 📦 EmojiStar = "\u2B50" // ⭐ EmojiSuccess = "\u2705" // ✅ EmojiCross = "\u274C" // ❌ EmojiRefresh = "\U0001F504" // 🔄 EmojiFile = "\U0001F4C4" // 📄 EmojiFolder = "\U0001F4C1" // 📁 EmojiCalendar = "\U0001F4C5" // 📅 EmojiUpArrow = "\u2B06\uFE0F" // ⬆️ EmojiDownArrow = "\u2B07\uFE0F" // ⬇️ EmojiInfo = "\u2139\uFE0F" // ℹ️ EmojiQuestion = "\u2753" // ❓ EmojiLink = "\U0001F517" // 🔗 EmojiUser = "\U0001F464" // 👤 EmojiGroup = "\U0001F465" // 👥 EmojiPipeline = "\U0001F6A7" // 🚧 EmojiMergeRequest = "\U0001F5C3" // 🗃️ EmojiIssue = "\U0001F4A1" // 💡 EmojiRed = "\U0001F534" // 🔴 EmojiYellow = "\U0001F7E1" // 🟡 EmojiGreen = "\U0001F7E2" // 🟢 EmojiProhibited = "\U0001F6AB" // 🚫 EmojiWhiteCircle = "\u26AA" // ⚪ EmojiParty = "\U0001F389" // 🎉 EmojiPurple = "\U0001F7E3" // 🟣 EmojiBlue = "\U0001F535" // 🔵 EmojiStop = "\u26D4" // ⛔ EmojiSkip = "\u23ED\uFE0F" // ⏭️ EmojiNew = "\U0001F195" // 🆕 EmojiHand = "\u270B" // ✋ )
Contextual emoji constants for consistent visual indicators across formatters.
const ( PollMinInterval = 5 PollMaxInterval = 60 PollDefaultInterval = 10 PollMinTimeout = 1 PollMaxTimeout = 3600 PollDefaultTimeout = 300 )
const (
DefaultMaxFileSize = config.DefaultMaxFileSize
)
Upload size defaults re-exported from config (single source of truth).
const ErrMsgContextCanceled = "context canceled"
ErrMsgContextCanceled is the operation label passed to WrapErr when a tool handler detects context cancellation before calling the GitLab API.
const FmtMdH3 = "### %s\n\n"
FmtMdH3 is a heading-3 format string with trailing blank line.
const HintPreserveLinks = "" /* 127-byte string literal not displayed */
HintPreserveLinks reminds the LLM to keep the clickable [text](url) markdown links when presenting list results to the user.
const StepFormattingResponse = "Formatting response..."
StepFormattingResponse is a standard progress step label for response formatting.
const TblFieldValue = "| Field | Value |\n| --- | --- |\n"
TblFieldValue is the standard "| Field | Value |" detail table header+separator.
Variables ¶
var ( ReadAnnotations = &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(true), } CreateAnnotations = &mcp.ToolAnnotations{ DestructiveHint: new(false), OpenWorldHint: new(true), } UpdateAnnotations = &mcp.ToolAnnotations{ DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(true), } DeleteAnnotations = &mcp.ToolAnnotations{ DestructiveHint: new(true), IdempotentHint: true, OpenWorldHint: new(true), } // NonDestructiveMetaAnnotations are for meta-tools that include // create/update operations but no delete actions. NonDestructiveMetaAnnotations = &mcp.ToolAnnotations{ DestructiveHint: new(false), OpenWorldHint: new(true), } // MetaAnnotations are annotations for meta-tools that combine read/write/delete. // Since a single meta-tool may include destructive actions, annotations reflect // the most cautious combination. MetaAnnotations = &mcp.ToolAnnotations{ DestructiveHint: new(true), OpenWorldHint: new(true), } // ReadOnlyMetaAnnotations are for meta-tools with only list/get/search actions. ReadOnlyMetaAnnotations = &mcp.ToolAnnotations{ ReadOnlyHint: true, DestructiveHint: new(false), IdempotentHint: true, OpenWorldHint: new(true), } )
Tool annotation presets for different operation categories. Each preset configures MCP hints that help LLMs understand whether a tool is read-only, destructive, or idempotent.
var ( // ContentBoth marks content for both user display and LLM processing (default). ContentBoth = &mcp.Annotations{ Audience: []mcp.Role{"user", "assistant"}, Priority: 0.5, } // ContentUser marks content primarily for user display (uploads, visualizations). ContentUser = &mcp.Annotations{ Audience: []mcp.Role{"user"}, Priority: 0.8, } // ContentAssistant marks content primarily for LLM processing (search, raw data). ContentAssistant = &mcp.Annotations{ Audience: []mcp.Role{"assistant"}, Priority: 0.7, } )
Content annotation presets for TextContent responses. These guide MCP clients on who the content is intended for and its importance.
var ( // ContentList marks list/search results for LLM processing with lower priority. ContentList = &mcp.Annotations{ Audience: []mcp.Role{"assistant"}, Priority: 0.4, } // ContentDetail marks single-entity details for LLM processing with medium priority. ContentDetail = &mcp.Annotations{ Audience: []mcp.Role{"assistant"}, Priority: 0.6, } // ContentMutate marks create/update/delete results for LLM processing with high priority. ContentMutate = &mcp.Annotations{ Audience: []mcp.Role{"assistant"}, Priority: 0.8, } )
Operation-based content annotation presets. All use audience ["assistant"] so the Markdown content is available to the LLM for reasoning, while StructuredContent (JSON) serves programmatic clients. This avoids redundant display when clients show both Content and StructuredContent.
var ( IconBranch = icon(svgBranch) IconCommit = icon(svgCommit) IconIssue = icon(svgIssue) IconMR = icon(svgMR) IconPipeline = icon(svgPipeline) IconJob = icon(svgJob) IconRelease = icon(svgRelease) IconTag = icon(svgTag) IconProject = icon(svgProject) IconGroup = icon(svgGroup) IconUser = icon(svgUser) IconWiki = icon(svgWiki) IconFile = icon(svgFile) IconPackage = icon(svgPackage) IconSearch = icon(svgSearch) IconLabel = icon(svgLabel) IconMilestone = icon(svgMilestone) IconEnvironment = icon(svgEnvironment) IconDeploy = icon(svgDeploy) IconSchedule = icon(svgSchedule) IconVariable = icon(svgVariable) IconRunner = icon(svgRunner) IconTodo = icon(svgTodo) IconHealth = icon(svgHealth) IconUpload = icon(svgUpload) IconBoard = icon(svgBoard) IconSnippet = icon(svgSnippet) IconToken = icon(svgToken) IconIntegration = icon(svgIntegration) IconNotify = icon(svgNotify) IconServer = icon(svgServer) IconSecurity = icon(svgSecurity) IconConfig = icon(svgConfig) IconAnalytics = icon(svgAnalytics) IconKey = icon(svgKey) IconLink = icon(svgLink) IconDiscussion = icon(svgDiscussion) IconEvent = icon(svgEvent) IconContainer = icon(svgContainer) IconImport = icon(svgImport) IconAlert = icon(svgAlert) IconTemplate = icon(svgTemplate) IconInfra = icon(svgInfra) IconEpic = icon(svgEpic) )
Domain icons — each returns a one-element []mcp.Icon ready for the Icons field.
Functions ¶
func AccessLevelDescription ¶
func AccessLevelDescription(level gl.AccessLevelValue) string
AccessLevelDescription maps GitLab access level integers to human-readable labels.
func AdjustPagination ¶
func AdjustPagination(p *PaginationOutput, itemCount int)
AdjustPagination corrects pagination metadata when the GitLab API does not return X-Total and X-Total-Pages headers (e.g., the Search API). It infers TotalItems and TotalPages from the actual item count received and the presence of a NextPage indicator.
func AppendResourceLink
deprecated
func AppendResourceLink(_ *mcp.CallToolResult, _, _, _ string)
Deprecated: AppendResourceLink is intentionally a no-op. It previously emitted mcp.ResourceLink content blocks with external HTTP URLs (GitLab WebURL), but ResourceLink is reserved for MCP-registered resources (gitlab:// URIs). Clients that received an https:// ResourceLink attempted to resolve it via resources/read, triggering JSON-RPC -32002 "Resource not found" errors. External web links are already included in the Markdown text output. Callers will be removed in a future major version.
func BuildTargetURL ¶
BuildTargetURL constructs a GitLab web URL for a target resource. Returns "" when the project web URL is empty, the IID is zero, or the target type has no known URL segment.
Supported target types: Issue, MergeRequest, Milestone.
func CancelledResult ¶
func CancelledResult(message string) *mcp.CallToolResult
CancelledResult returns a non-error tool result indicating the user canceled.
func ClampPollInterval ¶ added in v1.0.2
ClampPollInterval constrains a polling interval to [PollMinInterval, PollMaxInterval], returning PollDefaultInterval when the value is below the minimum.
func ClampPollTimeout ¶ added in v1.0.2
ClampPollTimeout constrains a polling timeout to [PollMinTimeout, PollMaxTimeout], returning PollDefaultTimeout when the value is below the minimum.
func ClassifyError ¶
ClassifyError inspects the error chain and returns a short, human-friendly diagnostic message explaining what went wrong at a high level.
func ClassifyHTTPStatus ¶
ClassifyHTTPStatus returns a semantic description for common HTTP status codes.
func ComputeSHA256 ¶
ComputeSHA256 computes the SHA-256 checksum of a file at the given path and returns the lowercase hex-encoded hash string.
func ComputeSHA256Reader ¶
ComputeSHA256Reader computes the SHA-256 checksum from an arbitrary io.Reader and returns the lowercase hex-encoded hash string.
func ConfirmAction ¶
func ConfirmAction(ctx context.Context, req *mcp.CallToolRequest, message string) *mcp.CallToolResult
ConfirmAction uses MCP elicitation to ask the user for confirmation before a destructive action. Returns nil if the user confirmed or elicitation is unsupported (fallback: action proceeds). Returns a non-error tool result if the user declined or canceled.
func ConfirmDestructiveAction ¶
func ConfirmDestructiveAction(ctx context.Context, req *mcp.CallToolRequest, params map[string]any, message string) *mcp.CallToolResult
ConfirmDestructiveAction checks whether a destructive action should proceed. The confirmation flow is:
- YOLO_MODE / AUTOPILOT env var → skip confirmation entirely
- Explicit "confirm": true in params → skip confirmation
- MCP elicitation supported → ask user interactively
- Elicitation unsupported and no confirm param → return error prompting the caller to re-send with confirm: true
Returns nil if the action should proceed. Returns a non-nil *mcp.CallToolResult if the action was canceled or requires explicit confirmation.
func ContainsAny ¶
ContainsAny returns true if err.Error() contains any of the substrings.
func ContextWithRequest ¶
ContextWithRequest returns a derived context carrying the MCP request.
func DetectRichContent ¶
DetectRichContent scans a GFM body for non-portable features that may not render correctly outside GitLab (mermaid diagrams, math blocks, raw HTML). Returns a comma-separated list of detected features or an empty string.
func ErrFieldRequired ¶
ErrFieldRequired returns a validation error indicating that a required field is missing or empty. It produces the message "<field> is required", which is the standard validation pattern used across all tool handlers.
func ErrInvalidEnum ¶
ErrInvalidEnum returns a validation error indicating that a field value is not one of the allowed options. The error message lists the valid values to guide LLMs toward correct parameter usage.
func ErrRequiredInt64 ¶
ErrRequiredInt64 returns a formatted error when a required int64 field is missing or has its zero value. This catches silent deserialization failures in meta-tool dispatch, where a misnamed JSON parameter (e.g. "merge_request_iid" instead of "mr_iid") is silently ignored and the field defaults to 0.
func ErrRequiredString ¶
ErrRequiredString returns a formatted error when a required string field is missing or empty. Like ErrRequiredInt64, this guides LLMs to use the exact parameter name when silent deserialization failures occur.
func ErrorResult ¶
func ErrorResult(message string) *mcp.CallToolResult
ErrorResult builds a standard error mcp.CallToolResult with IsError set. It returns the error message as Markdown content for display.
func ErrorResultMarkdown ¶
func ErrorResultMarkdown(domain, action string, err error) *mcp.CallToolResult
ErrorResultMarkdown creates an MCP tool error result with Markdown formatting. The result has IsError = true for MCP clients that distinguish error results.
func EscapeMdHeading ¶
EscapeMdHeading sanitizes a user-controlled string that will be interpolated into a Markdown heading (e.g. `## Project: {name}`). It strips leading '#' characters that could promote/demote the heading level and collapses newlines into spaces so the heading stays on one line.
func EscapeMdTableCell ¶
EscapeMdTableCell escapes characters in s that would break a Markdown table row. Pipes are replaced with the HTML entity | and newlines/carriage-returns are replaced with a space so the cell stays on a single row.
func ExtractGitLabMessage ¶
ExtractGitLabMessage extracts the specific error message from a GitLab ErrorResponse in the error chain. Returns empty string if not found or if the message only repeats the HTTP status text (e.g., "405 Method Not Allowed"). The extracted message is truncated to 300 characters to prevent overly verbose error output from user-generated content.
func ExtractHints ¶
ExtractHints parses the "💡 Next steps" section from a Markdown tool response and returns the individual hint strings. Returns nil when the section is absent.
func FormatGID ¶
FormatGID builds a GitLab Global ID string from a type name and numeric ID.
FormatGID("Vulnerability", 42) → "gid://gitlab/Vulnerability/42"
func FormatGraphQLPagination ¶
func FormatGraphQLPagination(p GraphQLPaginationOutput, shown int) string
FormatGraphQLPagination renders cursor-based pagination metadata as a Markdown summary line, suitable for appending to list tool responses.
func FormatPagination ¶
func FormatPagination(p PaginationOutput) string
FormatPagination renders pagination metadata as a compact Markdown line.
func FormatTarget ¶
FormatTarget builds a Markdown table cell for a typed target resource. When targetURL is non-empty, the result is a clickable link like [Issue #42](url). When empty, the label is returned as plain text. Returns "" if there is nothing to display.
func FormatTime ¶
FormatTime converts an RFC3339 timestamp string to a human-readable format ("2 Jan 2006 15:04 UTC"). Returns the original string unchanged if parsing fails, so existing callers remain safe.
func IdentityToContext ¶
func IdentityToContext(ctx context.Context, id UserIdentity) context.Context
IdentityToContext stores a UserIdentity in the context. Used at startup in stdio mode to make the identity available to all tool handlers.
func ImageMIMEType ¶
ImageMIMEType returns the MIME type for image file extensions. Returns an empty string for non-image files.
func IsBinaryFile ¶
IsBinaryFile returns true when the filename has a known binary extension that is not an image. Returns false for text and image files.
func IsDestructiveAction ¶ added in v1.0.4
IsDestructiveAction reports whether the given meta-tool action name indicates a destructive operation that warrants user confirmation.
func IsHTTPStatus ¶
IsHTTPStatus reports whether err wraps a GitLab ErrorResponse with the given HTTP status code. Useful for handling specific API responses like 404 (feature not available on CE) or 403 (insufficient permissions).
func IsImageFile ¶
IsImageFile returns true when the filename has an image extension. Comparison is case-insensitive. Returns false for empty strings.
func IsNotFound ¶ added in v1.0.3
IsNotFound reports whether err represents a 404 Not Found, either via a structured GitLab ErrorResponse status code or via a plain-text error message from client-go (which may contain "404 Not Found" as text).
func IsTerminalStatus ¶ added in v1.0.2
IsTerminalStatus reports whether a CI/CD status represents a finished state.
func IsYOLOMode ¶
func IsYOLOMode() bool
IsYOLOMode returns true when destructive action confirmation should be skipped entirely. Checks the YOLO_MODE and AUTOPILOT environment variables. Any truthy value (1, true, yes — case-insensitive) enables the mode.
func IssueStateEmoji ¶
IssueStateEmoji returns the Markdown emoji for an issue state.
func LogToolCall ¶
LogToolCall logs a structured message after a tool handler completes. It records the tool name, elapsed duration, and any error that occurred.
func LogToolCallAll ¶
func LogToolCallAll(ctx context.Context, req *mcp.CallToolRequest, tool string, start time.Time, err error)
LogToolCallAll logs to both stderr (slog) and the MCP client (protocol logging). It is the standard logging function for all tool handlers. When the request contains authenticated user identity (any mode), it includes the user in the log output for audit trail purposes.
func MRStateEmoji ¶
MRStateEmoji returns the Markdown emoji for a merge request state.
func MakeMetaHandler ¶
func MakeMetaHandler(toolName string, routes map[string]ActionFunc, formatResult FormatResultFunc) func(ctx context.Context, req *mcp.CallToolRequest, input MetaToolInput) (*mcp.CallToolResult, any, error)
MakeMetaHandler creates a generic MCP tool handler that dispatches to action routes. The formatResult function converts the action result into an MCP response. If formatResult is nil, a default JSON formatter is used.
Destructive actions (delete, remove, revoke, unprotect, etc.) are automatically intercepted with a user confirmation prompt via MCP elicitation before execution. Confirmation can be bypassed with YOLO_MODE/AUTOPILOT env vars or by passing "confirm": true in the action params.
func MarkdownForResult ¶
func MarkdownForResult(result any) *mcp.CallToolResult
MarkdownForResult resolves a tool output to its Markdown mcp.CallToolResult. Returns nil for nil input (caller should handle), returns nil when no formatter is registered for the concrete type.
func MdTitleLink ¶
MdTitleLink returns the title as a Markdown link if url is non-empty, otherwise returns the escaped title. Suitable for table cells.
func MergeVariables ¶
MergeVariables merges multiple variable maps into a single map. Later maps override earlier ones for duplicate keys.
func MetaToolSchema ¶
func MetaToolSchema(routes map[string]ActionFunc) map[string]any
MetaToolSchema builds a JSON Schema for a meta-tool with the action field constrained to an enum of valid action names extracted from the routes map. Setting this as Tool.InputSchema ensures the LLM sees the exact list of valid actions in the schema, enabling first-try action selection.
func NormalizeText ¶
NormalizeText replaces literal escape sequences with real characters. MCP clients may send text with literal backslash-n instead of real newlines when the JSON transport double-escapes the input.
Replacement order matters to avoid cascading conversions:
- `\\` -> `\` (double-escaped backslash first, so `\\n` becomes `\` + literal n, not a newline)
- `\r\n` -> `\n` (CRLF before individual CR/LF to avoid double-replacement)
- `\r` -> `\n` (standalone carriage return)
- `\n` -> newline (the most common case)
- `\t` -> tab
func NotFoundResult ¶
func NotFoundResult(resource, identifier string, hints ...string) *mcp.CallToolResult
NotFoundResult creates an informational MCP tool result for resources that do not exist or are not accessible. Instead of returning a Go error (which would be logged as ERROR and produce an opaque error message for the LLM), this returns a structured Markdown result with actionable next steps.
The result has IsError=true to signal the tool could not fulfill the request, but the content is rich and helpful — the LLM can act on the suggestions.
Use this in register.go handler closures when IsHTTPStatus(err, 404) is true for "get" operations. Pass nil error back to the SDK so the call is logged at INFO level instead of ERROR.
func OpenAndValidateFile ¶
OpenAndValidateFile opens a local file for reading after validating it exists, is a regular file (not a directory, symlink, device or pipe), and does not exceed maxSize bytes. Returns the open file handle and its FileInfo.
func ParseGID ¶
ParseGID extracts the type name and numeric ID from a GitLab Global ID. It returns an error if the format is invalid.
ParseGID("gid://gitlab/Vulnerability/42") → ("Vulnerability", 42, nil)
func ParseOptionalTime ¶
ParseOptionalTime parses an RFC3339 string and returns a *time.Time. Returns nil if the string is empty or unparseable.
func PipelineStatusEmoji ¶
PipelineStatusEmoji returns the Markdown emoji for a pipeline status.
func PopulateHints ¶
func PopulateHints(result *mcp.CallToolResult, setter HintSetter)
PopulateHints extracts next-step hints from the Markdown content of a CallToolResult and sets them on the output struct. It is a no-op when result is nil, contains no TextContent, or has no hints section.
func ProgressReportInterval ¶
ProgressReportInterval returns the byte interval between progress reports. It is the smaller of 1 MB or 5% of total, with a minimum of 64 KB.
func RegisterMarkdown ¶
RegisterMarkdown registers a Markdown string formatter for type T. Subsequent calls to MarkdownForResult with a value of type T will invoke fn and wrap the returned string in a mcp.CallToolResult.
func RegisterMarkdownResult ¶
func RegisterMarkdownResult[T any](fn func(T) *mcp.CallToolResult)
RegisterMarkdownResult registers a result formatter for type T. Use this for types that need custom mcp.CallToolResult construction (e.g. uploads with image content).
func RegisteredMarkdownTypeNames ¶
func RegisteredMarkdownTypeNames() []string
RegisteredMarkdownTypeNames returns the type names of all registered Markdown formatters (both string and result variants). Used by validation tests to verify sub-packages self-register their formatters.
func RequestFromContext ¶
func RequestFromContext(ctx context.Context) *mcp.CallToolRequest
RequestFromContext extracts the MCP request from a context, or nil if absent.
func RichContentHint ¶
RichContentHint returns an informational note directing users to the GitLab web URL for full rendering when non-portable GFM features are detected. Returns an empty string when features or webURL is empty.
func SetUploadConfig ¶
func SetUploadConfig(maxFileSize int64)
SetUploadConfig overrides the default upload thresholds. Call before RegisterAll to propagate values into tool handler closures.
func SuccessResult ¶
func SuccessResult(markdown string) *mcp.CallToolResult
SuccessResult builds a standard success mcp.CallToolResult with Markdown and the structured output for both human-readable and programmatic consumption. If markdown is empty, the result contains only a structured JSON annotation.
func TitleFromName ¶
TitleFromName generates a human-readable UI title from a snake_case MCP tool name by stripping the "gitlab_" prefix and converting to Title Case.
TitleFromName("gitlab_list_projects") // returns "List Projects"
func ToolResultAnnotated ¶
func ToolResultAnnotated(md string, ann *mcp.Annotations) *mcp.CallToolResult
ToolResultAnnotated wraps a Markdown string into a CallToolResult with content annotations that guide MCP clients on audience and priority. Pass nil annotations to get the same behavior as ToolResultWithMarkdown.
func ToolResultWithImage ¶
func ToolResultWithImage(md string, ann *mcp.Annotations, imageData []byte, mimeType string) *mcp.CallToolResult
ToolResultWithImage creates a CallToolResult containing both a text description (metadata) and an ImageContent block with the raw image bytes. Multimodal LLMs can "see" the image; text-only LLMs get the metadata.
func ToolResultWithMarkdown ¶
func ToolResultWithMarkdown(md string) *mcp.CallToolResult
ToolResultWithMarkdown wraps a Markdown string into a CallToolResult with a single TextContent entry annotated for assistant-only audience. This prevents MCP clients (e.g. VS Code) from displaying raw Markdown inline — the LLM processes it and presents formatted output to the user.
func UnmarshalParams ¶
UnmarshalParams re-serializes params map to JSON and deserializes into T. LLMs frequently send numeric values as JSON strings (e.g. "17" instead of 17). When standard unmarshalling fails, this function retries after coercing string values that look like integers or floats into actual numbers.
func ValidActionsString ¶
func ValidActionsString(routes map[string]ActionFunc) string
ValidActionsString returns a sorted, comma-separated list of action names.
func ValidateDiffPosition ¶
ValidateDiffPosition checks whether a (newLine, oldLine) combination corresponds to a valid commentable position in the parsed diff lines.
Rules enforced (per GitLab API):
- new_line only → line must be an added (+) line
- old_line only → line must be a removed (-) line
- both set → line must be an unchanged context line
Returns nil when the position is valid, or a descriptive error explaining exactly why the position is invalid and what the caller should do instead.
func ValidatePackageFileName ¶
ValidatePackageFileName validates a filename for GitLab generic package upload. Filenames must not be empty, must not contain spaces, and must not start with a tilde or at-sign.
func ValidatePackageName ¶
ValidatePackageName validates a GitLab generic package name against allowed characters. Names must start with a letter or digit and may contain A-Z a-z 0-9 . _ - + ~ / @.
func WithHints ¶
func WithHints[O any](result *mcp.CallToolResult, out O, err error) (*mcp.CallToolResult, O, error)
WithHints extracts hints from a CallToolResult and populates them on the typed output struct, returning all three handler values in one call. This avoids evaluation-order ambiguity in multi-value return statements.
For value Out types (the common case), &out is used internally to satisfy the HintSetter pointer receiver. For pointer Out types (*T), the pointer itself implements HintSetter. If neither case applies, WithHints is a no-op.
return toolutil.WithHints(toolutil.ToolResultWithMarkdown(md), out, err)
func WrapErr ¶
WrapErr classifies the error, enriches it with a semantic message, and wraps it with the operation name. All tool handlers funnel through here so connectivity and auth problems are reported consistently.
func WrapErrWithHint ¶
WrapErrWithHint works like WrapErrWithMessage but appends an actionable hint that tells the LLM what to do next. Example:
"branchDelete: bad request — Cannot delete: protected branch. Suggestion: use gitlab_branch_unprotect first, then retry deletion: <original>"
The hint should be a concise suggestion starting with a verb (e.g., "use gitlab_branch_list to verify the branch name").
func WrapErrWithMessage ¶
WrapErrWithMessage works like WrapErr but also includes the specific GitLab error message (from ErrorResponse.Message) when available. This produces richer errors like:
"fileCreate: bad request — {error: A file with this name already exists}: POST .../files: 400"
Use WrapErrWithMessage for mutating operations where the specific GitLab error detail helps the LLM understand what went wrong. Use WrapErr for read-only operations where the generic classification suffices.
func WrapGFMBody ¶
WrapGFMBody wraps user-generated GFM content in a Markdown blockquote to prevent heading hierarchy conflicts and structural breaks in the formatted output. Empty bodies return an empty string.
func WriteEmpty ¶
WriteEmpty writes a standardized empty-result message to the builder. The resource parameter should be a clear, specific plural noun (e.g. "merge requests", "pipeline variables", "protected branches").
func WriteHints ¶
WriteHints appends a "💡 Next steps" section to the Markdown builder. Each hint is a short string describing a related action the LLM can take (e.g. "Use action 'delete' to remove this package"). If no hints are provided, nothing is written.
func WriteListSummary ¶
func WriteListSummary(b *strings.Builder, shown int, p PaginationOutput)
WriteListSummary appends a brief "Showing N of M results (page X of Y)" line between the heading and the table body. It is a no-op when there is only a single page, because the heading count already conveys everything.
func WritePagination ¶
func WritePagination(b *strings.Builder, p PaginationOutput)
WritePagination appends a newline-wrapped pagination summary to the builder.
Types ¶
type ActionFunc ¶
ActionFunc is a handler that receives raw params and returns a result or error.
func WrapAction ¶
func WrapAction[T any, R any](client *gitlabclient.Client, fn func(ctx context.Context, client *gitlabclient.Client, input T) (R, error)) ActionFunc
WrapAction wraps a typed handler (input T -> output R) into a generic ActionFunc.
func WrapActionWithRequest ¶
func WrapActionWithRequest[T any, R any](client *gitlabclient.Client, fn func(ctx context.Context, req *mcp.CallToolRequest, client *gitlabclient.Client, input T) (R, error)) ActionFunc
WrapActionWithRequest wraps a handler that also requires the MCP request (e.g., for progress tracking). The request is extracted from context via RequestFromContext; if absent, nil is passed.
func WrapVoidAction ¶
func WrapVoidAction[T any](client *gitlabclient.Client, fn func(ctx context.Context, client *gitlabclient.Client, input T) error) ActionFunc
WrapVoidAction wraps a typed handler that returns only error.
type DeleteOutput ¶
type DeleteOutput struct {
HintableOutput
Status string `json:"status"`
Message string `json:"message"`
}
DeleteOutput is a confirmation message returned by destructive tool handlers (delete, unprotect, unapprove) so the LLM receives explicit feedback instead of empty content when the operation succeeds.
func DeleteResult ¶
func DeleteResult(resource string) (*mcp.CallToolResult, DeleteOutput, error)
DeleteResult builds a DeleteOutput and its Markdown representation for a successful destructive operation. The resource parameter describes what was affected (e.g., "project 42", "branch feature/x").
type DetailedError ¶
type DetailedError struct {
Domain string `json:"domain"`
Action string `json:"action"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
GitLabStatus int `json:"gitlab_status,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
DetailedError represents a rich, structured error with domain context for diagnostic output. It extends ToolError with additional fields useful for automated issue creation and Markdown error reporting.
func NewDetailedError ¶
func NewDetailedError(domain, action string, err error) *DetailedError
NewDetailedError creates a DetailedError from a GitLab API error, extracting HTTP status and request ID when available.
func (*DetailedError) Error ¶
func (e *DetailedError) Error() string
Error returns a concise representation: "domain/action: message".
func (*DetailedError) Markdown ¶
func (e *DetailedError) Markdown() string
Markdown renders the error as a Markdown block suitable for display in MCP tool results. Includes all available context for diagnostics.
type DiffLine ¶
type DiffLine struct {
OldLine int // Line number in old file (0 for added lines)
NewLine int // Line number in new file (0 for removed lines)
Type LineType // Whether the line was added, removed, or unchanged
}
DiffLine represents a single line in a parsed unified diff with its old/new line numbers and type (added, removed, or context).
func ParseDiffLines ¶
ParseDiffLines parses a unified diff string and returns metadata for each line, including old/new line numbers and whether it is added, removed, or context. Only lines inside @@ hunk headers are returned.
type DiffOutput ¶
type DiffOutput struct {
OldPath string `json:"old_path"`
NewPath string `json:"new_path"`
AMode string `json:"a_mode,omitempty"`
BMode string `json:"b_mode,omitempty"`
Diff string `json:"diff"`
NewFile bool `json:"new_file"`
RenamedFile bool `json:"renamed_file"`
DeletedFile bool `json:"deleted_file"`
}
DiffOutput represents a single file diff from the GitLab API. It is used by both commit diff and repository compare operations.
func DiffToOutput ¶
func DiffToOutput(d *gl.Diff) DiffOutput
DiffToOutput converts a GitLab API gl.Diff to the MCP tool output format.
type FormatResultFunc ¶
type FormatResultFunc func(any) *mcp.CallToolResult
FormatResultFunc converts an action result into an MCP call tool result.
type GraphQLPageInfo ¶
type GraphQLPageInfo struct {
HasNextPage bool `json:"has_next_page"`
HasPreviousPage bool `json:"has_previous_page"`
EndCursor string `json:"end_cursor,omitempty"`
StartCursor string `json:"start_cursor,omitempty"`
}
GraphQLPageInfo holds cursor-based pagination metadata returned by GraphQL connection responses. It maps directly to GitLab's PageInfo type.
type GraphQLPaginationInput ¶
type GraphQLPaginationInput struct {
First *int `json:"first,omitempty" jsonschema:"Number of items to return (default 20, max 100)"`
After string `json:"after,omitempty" jsonschema:"Cursor for forward pagination (from previous response end_cursor)"`
Last *int `json:"last,omitempty" jsonschema:"Number of items from the end (backward pagination)"`
Before string `json:"before,omitempty" jsonschema:"Cursor for backward pagination (from previous response start_cursor)"`
}
GraphQLPaginationInput holds cursor-based pagination parameters for GraphQL list queries. It mirrors the standard GraphQL connection model (first/after for forward, last/before for backward).
func (GraphQLPaginationInput) EffectiveFirst ¶
func (p GraphQLPaginationInput) EffectiveFirst() int
EffectiveFirst returns the requested page size, clamped to [1, GraphQLMaxFirst] with GraphQLDefaultFirst as fallback.
func (GraphQLPaginationInput) Variables ¶
func (p GraphQLPaginationInput) Variables() map[string]any
Variables returns a map suitable for inclusion in a GraphQL variables payload. Only non-zero fields are included.
type GraphQLPaginationOutput ¶
type GraphQLPaginationOutput struct {
HasNextPage bool `json:"has_next_page"`
HasPreviousPage bool `json:"has_previous_page"`
EndCursor string `json:"end_cursor,omitempty"`
StartCursor string `json:"start_cursor,omitempty"`
}
GraphQLPaginationOutput holds pagination metadata for GraphQL list tool responses, presented in a consistent format for LLM consumers.
func PageInfoToOutput ¶
func PageInfoToOutput(pi GraphQLRawPageInfo) GraphQLPaginationOutput
PageInfoToOutput converts a raw GraphQL PageInfo response struct (with camelCase JSON keys from the API) to the snake_case output struct.
type GraphQLRawPageInfo ¶
type GraphQLRawPageInfo struct {
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
EndCursor string `json:"endCursor"`
StartCursor string `json:"startCursor"`
}
GraphQLRawPageInfo matches the camelCase JSON shape returned by the GitLab GraphQL API before conversion to our snake_case output.
type HintSetter ¶
type HintSetter interface {
// SetNextSteps stores the extracted next-step hints on the output struct.
SetNextSteps(hints []string)
}
HintSetter is implemented by any Output struct that embeds HintableOutput. PopulateHints uses this interface to set extracted hints on the output.
type HintableOutput ¶
type HintableOutput struct {
NextSteps []string `json:"next_steps,omitempty"`
}
HintableOutput is an embeddable struct that adds a next_steps field to any Output type. Embed it as the FIRST field of an Output struct so that next_steps appears first in the serialized JSON, giving LLMs immediate guidance before reading the rest of the payload.
type Output struct {
toolutil.HintableOutput
Name string `json:"name"`
}
func (*HintableOutput) SetNextSteps ¶
func (h *HintableOutput) SetNextSteps(hints []string)
SetNextSteps stores the given hints in the NextSteps field.
type MetaToolInput ¶
type MetaToolInput struct {
Action string `json:"action" jsonschema:"Action to perform. See the tool description for available actions and their parameters."`
Params map[string]any `` /* 147-byte string literal not displayed */
}
MetaToolInput is the common input for all meta-tools. The LLM sends an action name and a params object; the dispatcher routes to the underlying handler function and deserializes params into the action-specific input struct.
type PaginationInput ¶
type PaginationInput struct {
Page int `json:"page,omitempty" jsonschema:"Page number (default 1)"`
PerPage int `json:"per_page,omitempty" jsonschema:"Items per page (default 20, max 100)"`
}
PaginationInput holds common pagination query parameters for list endpoints.
type PaginationOutput ¶
type PaginationOutput struct {
Page int64 `json:"page"`
PerPage int64 `json:"per_page"`
TotalItems int64 `json:"total_items"`
TotalPages int64 `json:"total_pages"`
NextPage int64 `json:"next_page"`
PrevPage int64 `json:"prev_page"`
}
PaginationOutput holds pagination metadata extracted from GitLab API responses. Fields map to GitLab's X-Page, X-Per-Page, X-Total, X-Total-Pages, X-Next-Page, X-Prev-Page headers.
func PaginationFromResponse ¶
func PaginationFromResponse(resp *gl.Response) PaginationOutput
PaginationFromResponse extracts pagination metadata from a GitLab API response.
type ProgressReader ¶
type ProgressReader struct {
// contains filtered or unexported fields
}
ProgressReader wraps an io.Reader and reports progress to an MCP progress tracker as bytes are read. Safe to use with a zero-value/inactive tracker.
func NewProgressReader ¶
func NewProgressReader(ctx context.Context, r io.Reader, total int64, tracker progress.Tracker) *ProgressReader
NewProgressReader creates a ProgressReader that reports upload progress. If the tracker is inactive, the wrapper still works but skips notifications.
func (*ProgressReader) BytesRead ¶
func (pr *ProgressReader) BytesRead() int64
BytesRead returns the total number of bytes read so far.
type ProgressWriter ¶
type ProgressWriter struct {
// contains filtered or unexported fields
}
ProgressWriter wraps an io.Writer and reports progress to an MCP progress tracker as bytes are written (used for downloads to disk).
func NewProgressWriter ¶
func NewProgressWriter(ctx context.Context, w io.Writer, total int64, tracker progress.Tracker) *ProgressWriter
NewProgressWriter creates a ProgressWriter that reports download progress.
func (*ProgressWriter) BytesWritten ¶
func (pw *ProgressWriter) BytesWritten() int64
BytesWritten returns the total number of bytes written so far.
type StringOrInt ¶
type StringOrInt string //nolint:recvcheck // UnmarshalJSON requires pointer receiver, others are value receivers by design
StringOrInt is a string type that accepts both JSON strings and JSON numbers during unmarshalling. It always stores the value as a string internally. This is needed because LLMs frequently send numeric IDs (e.g. 405) as JSON numbers rather than strings, even when the schema declares "type": "string".
func (StringOrInt) Int64 ¶
func (s StringOrInt) Int64() (int64, error)
Int64 parses the stored string as a base-10 integer and returns it. Returns 0 and an error if the value is empty or not a valid integer.
func (StringOrInt) MarshalJSON ¶
func (s StringOrInt) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler to always output a JSON string.
func (StringOrInt) String ¶
func (s StringOrInt) String() string
String returns the underlying string value.
func (*StringOrInt) UnmarshalJSON ¶
func (s *StringOrInt) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler to accept both JSON strings (e.g. "405", "group/project") and JSON numbers (e.g. 405, 42.0).
type ToolError ¶
type ToolError struct {
Tool string `json:"tool"`
Message string `json:"message"`
StatusCode int `json:"status_code,omitempty"`
}
ToolError represents a structured error from a tool handler.
type UploadConfig ¶
type UploadConfig struct {
MaxFileSize int64
}
UploadConfig holds runtime-configurable upload parameters. Initialized with package defaults; use SetUploadConfig to override from environment config.
func GetUploadConfig ¶
func GetUploadConfig() UploadConfig
GetUploadConfig returns the current upload configuration (for testing).
type UserIdentity ¶
UserIdentity holds the authenticated user's identity. Populated from OAuth TokenInfo (HTTP modes) or from the startup-resolved identity stored in context (stdio mode).
func IdentityFromContext ¶
func IdentityFromContext(ctx context.Context) UserIdentity
IdentityFromContext retrieves the UserIdentity stored in the context. Returns a zero-value UserIdentity if none was stored.
func ResolveIdentity ¶
func ResolveIdentity(ctx context.Context, req *mcp.CallToolRequest) UserIdentity
ResolveIdentity returns the authenticated user's identity by checking two sources in priority order:
- req.Extra.TokenInfo (populated by SDK in HTTP modes via RequireBearerToken)
- Context-stored identity (populated at startup in stdio mode)
Returns a zero-value UserIdentity if neither source has identity.
func (UserIdentity) IsAuthenticated ¶
func (u UserIdentity) IsAuthenticated() bool
IsAuthenticated returns true if the identity contains a non-empty UserID.
type VoidOutput ¶
type VoidOutput struct {
HintableOutput
Status string `json:"status"`
Message string `json:"message"`
}
VoidOutput is a confirmation message returned by tool handlers that perform an action without returning domain data (e.g., set header, start mirroring).
func VoidResult ¶
func VoidResult(message string) (*mcp.CallToolResult, VoidOutput, error)
VoidResult builds a VoidOutput and its Markdown representation for a successful void operation. The message describes what happened.