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
38 changes: 38 additions & 0 deletions pkg/gateway/mcp/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,44 @@ import (
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
)

// JSONResultWithProjectID creates a CallToolResult containing the JSON-encoded
// value with an additional "active_project_id" field merged into the top-level
// object. This allows agents to self-verify that results came from the intended
// project. If projectID is empty, the result is identical to JSONResult.
func JSONResultWithProjectID(v any, projectID string) (*mcpsdk.CallToolResult, error) {
data, err := json.Marshal(v)
if err != nil {
return nil, err
}
if projectID == "" {
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: string(data)},
},
}, nil
}
var m map[string]json.RawMessage
if err := json.Unmarshal(data, &m); err != nil {
// v is not a JSON object; return as-is
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: string(data)},
},
}, nil
}
pid, _ := json.Marshal(projectID)
m["active_project_id"] = pid
out, err := json.Marshal(m)
if err != nil {
return nil, err
}
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: string(out)},
},
}, nil
}

// JSONResult creates a CallToolResult containing the JSON-encoded value as
// text content. This is the standard way to return structured data from a
// tool handler.
Expand Down
2 changes: 1 addition & 1 deletion pkg/gateway/mcp/tool_connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func connectionsList(ctx context.Context, client *hookdeck.Client, in input) (*m
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}

func connectionsGet(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/gateway/mcp/tool_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func eventsList(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}

func eventsGet(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) {
Expand Down
22 changes: 22 additions & 0 deletions pkg/gateway/mcp/tool_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func helpOverview(client *hookdeck.Client) *mcpsdk.CallToolResult {

Current project: %s

All tools operate on the active project. Call hookdeck_projects first when the user
references a project by name, or when unsure which project is active.

hookdeck_projects — List or switch projects (actions: list, use)
hookdeck_connections — Inspect connections and control delivery flow (actions: list, get, pause, unpause)
hookdeck_sources — Inspect inbound sources (actions: list, get)
Expand All @@ -55,6 +58,12 @@ Use hookdeck_help with topic="<tool_name>" for detailed help on a specific tool.
var toolHelp = map[string]string{
"hookdeck_projects": `hookdeck_projects — List or switch the active project

Always call this first when the user references a specific project by name. List available
projects to find the matching project ID, then use the "use" action to switch to it before
calling any other tools. All queries (events, issues, connections, metrics, requests) are
scoped to the active project — if the wrong project is active, all results will be wrong.
Also use this when unsure which project is currently active.

Actions:
list — List all projects. Returns id, org, project, type (gateway/outpost/console), and which is current. Outbound projects are excluded.
use — Switch the active project for this session (in-memory only).
Expand All @@ -65,6 +74,8 @@ Parameters:

"hookdeck_connections": `hookdeck_connections — Inspect connections and control delivery flow

Results are scoped to the active project — call hookdeck_projects first if the user has specified a project.

Actions:
list — List connections with optional filters
get — Get a single connection by ID
Expand Down Expand Up @@ -122,6 +133,8 @@ Parameters:

"hookdeck_requests": `hookdeck_requests — Query inbound requests

Results are scoped to the active project — call hookdeck_projects first if the user has specified a project.

Actions:
list — List requests with optional filters
get — Get a single request by ID
Expand All @@ -141,6 +154,8 @@ Parameters:

"hookdeck_events": `hookdeck_events — Query events (processed deliveries)

Results are scoped to the active project — call hookdeck_projects first if the user has specified a project.

Actions:
list — List events with optional filters
get — Get a single event by ID (metadata and headers only; no payload)
Expand Down Expand Up @@ -180,6 +195,8 @@ Parameters:

"hookdeck_issues": `hookdeck_issues — Inspect aggregated failure signals

Results are scoped to the active project — call hookdeck_projects first if the user has specified a project.

Actions:
list — List issues with optional filters
get — Get a single issue by ID
Expand All @@ -197,6 +214,8 @@ Parameters:

"hookdeck_metrics": `hookdeck_metrics — Query aggregate metrics

Results are scoped to the active project — call hookdeck_projects first if the user has specified a project.

Actions:
events — Event metrics (auto-routes to queue-depth, pending, or by-issue as needed)
requests — Request metrics
Expand All @@ -218,6 +237,9 @@ Parameters:

"hookdeck_help": `hookdeck_help — Get an overview of available tools or detailed help for a specific tool

Note: all tools operate on the active project — use hookdeck_projects to verify or switch
project context before querying.

Parameters:
topic (string) — Tool name for detailed help (e.g. "hookdeck_events"). Omit for overview.`,
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/gateway/mcp/tool_issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func issuesList(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}

func issuesGet(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) {
Expand Down
8 changes: 4 additions & 4 deletions pkg/gateway/mcp/tool_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func metricsEvents(ctx context.Context, client *hookdeck.Client, in input) (*mcp
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}

func metricsRequests(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) {
Expand All @@ -107,7 +107,7 @@ func metricsRequests(ctx context.Context, client *hookdeck.Client, in input) (*m
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}

func metricsAttempts(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) {
Expand All @@ -119,7 +119,7 @@ func metricsAttempts(ctx context.Context, client *hookdeck.Client, in input) (*m
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}

func metricsTransformations(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) {
Expand All @@ -131,5 +131,5 @@ func metricsTransformations(ctx context.Context, client *hookdeck.Client, in inp
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}
2 changes: 1 addition & 1 deletion pkg/gateway/mcp/tool_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func requestsList(ctx context.Context, client *hookdeck.Client, in input) (*mcps
if err != nil {
return ErrorResult(TranslateAPIError(err)), nil
}
return JSONResult(result)
return JSONResultWithProjectID(result, client.ProjectID)
}

func requestsGet(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) {
Expand Down
14 changes: 7 additions & 7 deletions pkg/gateway/mcp/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func toolDefs(client *hookdeck.Client) []struct {
{
tool: &mcpsdk.Tool{
Name: "hookdeck_projects",
Description: "List available Hookdeck projects or switch the active project for this session. Use this to see which project you're querying and to change project context.",
Description: "Always call this first when the user references a specific project by name. List available projects to find the matching project ID, then use the `use` action to switch to it before calling any other tools. All queries (events, issues, connections, metrics, requests) are scoped to the active project — if the wrong project is active, all results will be wrong. Also use this when unsure which project is currently active.",
InputSchema: schema(map[string]prop{
"action": {Type: "string", Desc: "Action to perform: list or use", Enum: []string{"list", "use"}},
"project_id": {Type: "string", Desc: "Project ID (required for use action)"},
Expand All @@ -33,7 +33,7 @@ func toolDefs(client *hookdeck.Client) []struct {
{
tool: &mcpsdk.Tool{
Name: "hookdeck_connections",
Description: "Inspect connections (routes linking sources to destinations). List connections with filters, get details by ID, or pause/unpause a connection's delivery pipeline.",
Description: "Inspect connections (routes linking sources to destinations). List connections with filters, get details by ID, or pause/unpause a connection's delivery pipeline. Results are scoped to the active project — call `hookdeck_projects` first if the user has specified a project.",
InputSchema: schema(map[string]prop{
"action": {Type: "string", Desc: "Action: list, get, pause, or unpause", Enum: []string{"list", "get", "pause", "unpause"}},
"id": {Type: "string", Desc: "Connection ID (required for get/pause/unpause)"},
Expand Down Expand Up @@ -96,7 +96,7 @@ func toolDefs(client *hookdeck.Client) []struct {
{
tool: &mcpsdk.Tool{
Name: "hookdeck_requests",
Description: "Query inbound requests (raw HTTP data received by Hookdeck before routing). List with filters, get details, inspect the raw body, or view the events and ignored events generated from a request.",
Description: "Query inbound requests (raw HTTP data received by Hookdeck before routing). List with filters, get details, inspect the raw body, or view the events and ignored events generated from a request. Results are scoped to the active project — call `hookdeck_projects` first if the user has specified a project.",
InputSchema: schema(map[string]prop{
"action": {Type: "string", Desc: "Action: list, get, raw_body, events, or ignored_events", Enum: []string{"list", "get", "raw_body", "events", "ignored_events"}},
"id": {Type: "string", Desc: "Request ID (required for get/raw_body/events/ignored_events)"},
Expand All @@ -114,7 +114,7 @@ func toolDefs(client *hookdeck.Client) []struct {
{
tool: &mcpsdk.Tool{
Name: "hookdeck_events",
Description: "Query events (processed deliveries routed through connections to destinations). List with filters by status, source, destination, or date range. Get event details (get) or the event payload (raw_body). Use action raw_body with the event id to get the payload directly — do not use hookdeck_requests for the payload when you already have an event id.",
Description: "Query events (processed deliveries routed through connections to destinations). List with filters by status, source, destination, or date range. Get event details (get) or the event payload (raw_body). Use action raw_body with the event id to get the payload directly — do not use hookdeck_requests for the payload when you already have an event id. Results are scoped to the active project — call `hookdeck_projects` first if the user has specified a project.",
InputSchema: schema(map[string]prop{
"action": {Type: "string", Desc: "Action: list, get, or raw_body. Use raw_body to get the event payload (body); get returns metadata and headers only.", Enum: []string{"list", "get", "raw_body"}},
"id": {Type: "string", Desc: "Event ID (required for get/raw_body). Use with raw_body to fetch the event payload without querying the request."},
Expand Down Expand Up @@ -156,7 +156,7 @@ func toolDefs(client *hookdeck.Client) []struct {
{
tool: &mcpsdk.Tool{
Name: "hookdeck_issues",
Description: "List and inspect Hookdeck issues — aggregated failure signals such as repeated delivery failures, transformation errors, and backpressure alerts. Use this to identify systemic problems across your event pipeline.",
Description: "List and inspect Hookdeck issues — aggregated failure signals such as repeated delivery failures, transformation errors, and backpressure alerts. Use this to identify systemic problems across your event pipeline. Results are scoped to the active project — call `hookdeck_projects` first if the user has specified a project.",
InputSchema: schema(map[string]prop{
"action": {Type: "string", Desc: "Action: list or get", Enum: []string{"list", "get"}},
"id": {Type: "string", Desc: "Issue ID (required for get)"},
Expand All @@ -175,7 +175,7 @@ func toolDefs(client *hookdeck.Client) []struct {
{
tool: &mcpsdk.Tool{
Name: "hookdeck_metrics",
Description: "Query aggregate metrics over a time range. Get counts, failure rates, error rates, queue depth, and pending event data for events, requests, attempts, and transformations. Supports grouping by dimensions like source, destination, or connection.",
Description: "Query aggregate metrics over a time range. Get counts, failure rates, error rates, queue depth, and pending event data for events, requests, attempts, and transformations. Supports grouping by dimensions like source, destination, or connection. Results are scoped to the active project — call `hookdeck_projects` first if the user has specified a project.",
InputSchema: schema(map[string]prop{
"action": {Type: "string", Desc: "Metric type: events, requests, attempts, or transformations", Enum: []string{"events", "requests", "attempts", "transformations"}},
"start": {Type: "string", Desc: "Start datetime (ISO 8601, required)"},
Expand All @@ -195,7 +195,7 @@ func toolDefs(client *hookdeck.Client) []struct {
{
tool: &mcpsdk.Tool{
Name: "hookdeck_help",
Description: "Get an overview of all available Hookdeck tools or detailed help for a specific tool. Use this when unsure which tool to use for a task.",
Description: "Get an overview of all available Hookdeck tools or detailed help for a specific tool. Use this when unsure which tool to use for a task. Note: all tools operate on the active project — use `hookdeck_projects` to verify or switch project context before querying.",
InputSchema: schema(map[string]prop{
"topic": {Type: "string", Desc: "Tool name for detailed help (e.g. hookdeck_events). Omit for overview."},
}),
Expand Down
Loading