From 4e62908dcf4b5e96f0b89750c6ebcb640d5037b0 Mon Sep 17 00:00:00 2001 From: Paul Martins Date: Wed, 11 Feb 2026 16:20:26 -0500 Subject: [PATCH 1/5] Add service dependency support --- README.md | 2 + src/cmd/root.go | 120 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/README.md b/README.md index 4eda29a..0665188 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Currently, the MCP server only uses read-only access to your OpsLevel account an - Campaigns - Checks - Components +- Component Dependencies (services that a component depends on) +- Component Dependents (services that depend on a component) - Documentation (API & Tech Docs) - Domains - Filters diff --git a/src/cmd/root.go b/src/cmd/root.go index 4e69127..7db8073 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -107,6 +107,14 @@ type serializedCampaign struct { Reminder *opslevel.CampaignReminder } +type serializedDependency struct { + Id string + ServiceId string + Aliases []string + Locked bool + Notes string +} + // AccountMetadata represents the different types of account metadata that can be fetched type AccountMetadata string @@ -768,6 +776,118 @@ For complete reference: return newToolResult(campaigns, err) }) + // Register component dependencies tool + s.AddTool( + mcp.NewTool( + "componentDependencies", + mcp.WithDescription("Get all the services that a specific component depends on. Returns the dependency graph showing which services this component consumes or calls."), + mcp.WithString("serviceId", mcp.Required(), mcp.Description("The id of the service to fetch dependencies for.")), + mcp.WithString("search", mcp.Description("Optional search term to filter dependencies by name.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: "Component Dependencies in OpsLevel", + ReadOnlyHint: &trueValue, + DestructiveHint: &falseValue, + IdempotentHint: &trueValue, + OpenWorldHint: &trueValue, + }), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + serviceId, err := req.RequireString("serviceId") + if err != nil { + return mcp.NewToolResultError("serviceId parameter is required"), nil + } + + service := opslevel.Service{ + ServiceId: opslevel.ServiceId{ + Id: opslevel.ID(serviceId), + }, + } + + search := req.GetString("search", "") + variables := &opslevel.PayloadVariables{ + "after": "", + "first": 100, + } + if search != "" { + (*variables)["search"] = search + } + + resp, err := service.GetDependencies(client, variables) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get dependencies", err), nil + } + + var dependencies []serializedDependency + for _, edge := range resp.Edges { + dep := serializedDependency{ + Id: string(edge.Id), + ServiceId: string(edge.Node.Id), + Aliases: edge.Node.Aliases, + Locked: edge.Locked, + Notes: edge.Notes, + } + dependencies = append(dependencies, dep) + } + + return newToolResult(dependencies, nil) + }) + + // Register component dependents tool + s.AddTool( + mcp.NewTool( + "componentDependents", + mcp.WithDescription("Get all the services that depend on a specific component. Returns the reverse dependency graph showing which services consume or call this component."), + mcp.WithString("serviceId", mcp.Required(), mcp.Description("The id of the service to fetch dependents for.")), + mcp.WithString("search", mcp.Description("Optional search term to filter dependents by name.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: "Component Dependents in OpsLevel", + ReadOnlyHint: &trueValue, + DestructiveHint: &falseValue, + IdempotentHint: &trueValue, + OpenWorldHint: &trueValue, + }), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + serviceId, err := req.RequireString("serviceId") + if err != nil { + return mcp.NewToolResultError("serviceId parameter is required"), nil + } + + service := opslevel.Service{ + ServiceId: opslevel.ServiceId{ + Id: opslevel.ID(serviceId), + }, + } + + search := req.GetString("search", "") + variables := &opslevel.PayloadVariables{ + "after": "", + "first": 100, + } + if search != "" { + (*variables)["search"] = search + } + + resp, err := service.GetDependents(client, variables) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get dependents", err), nil + } + + var dependents []serializedDependency + for _, edge := range resp.Edges { + dep := serializedDependency{ + Id: string(edge.Id), + ServiceId: string(edge.Node.Id), + Aliases: edge.Node.Aliases, + Locked: edge.Locked, + Notes: edge.Notes, + } + dependents = append(dependents, dep) + } + + return newToolResult(dependents, nil) + }) + log.Info().Msg("Starting MCP server...") if err := server.ServeStdio(s); err != nil { if err == context.Canceled { From c81cb70645eb88d8d9c03c2743988cb63152007f Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Wed, 18 Feb 2026 09:24:17 -0500 Subject: [PATCH 2/5] Improve service dependency tools with validation and cleaner output - Validate service exists via GetService before fetching dependencies/dependents - Rename ServiceId field to ComponentId for clarity - Remove non-functional search parameter from both tools - Remove Locked field from serialized output - Use empty slice instead of nil for consistent JSON output Co-Authored-By: Claude Sonnet 4.6 --- src/cmd/root.go | 61 +++++++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/src/cmd/root.go b/src/cmd/root.go index 7db8073..e1a6c87 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -108,11 +108,10 @@ type serializedCampaign struct { } type serializedDependency struct { - Id string - ServiceId string - Aliases []string - Locked bool - Notes string + Id string + ComponentId string + Aliases []string + Notes string } // AccountMetadata represents the different types of account metadata that can be fetched @@ -782,7 +781,6 @@ For complete reference: "componentDependencies", mcp.WithDescription("Get all the services that a specific component depends on. Returns the dependency graph showing which services this component consumes or calls."), mcp.WithString("serviceId", mcp.Required(), mcp.Description("The id of the service to fetch dependencies for.")), - mcp.WithString("search", mcp.Description("Optional search term to filter dependencies by name.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: "Component Dependencies in OpsLevel", ReadOnlyHint: &trueValue, @@ -797,34 +795,31 @@ For complete reference: return mcp.NewToolResultError("serviceId parameter is required"), nil } - service := opslevel.Service{ - ServiceId: opslevel.ServiceId{ - Id: opslevel.ID(serviceId), - }, + service, err := client.GetService(serviceId) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get service", err), nil + } + if service.Id == "" { + return mcp.NewToolResultError(fmt.Sprintf("service with id %s not found", serviceId)), nil } - search := req.GetString("search", "") variables := &opslevel.PayloadVariables{ "after": "", "first": 100, } - if search != "" { - (*variables)["search"] = search - } resp, err := service.GetDependencies(client, variables) if err != nil { return mcp.NewToolResultErrorFromErr("failed to get dependencies", err), nil } - var dependencies []serializedDependency + dependencies := []serializedDependency{} for _, edge := range resp.Edges { dep := serializedDependency{ - Id: string(edge.Id), - ServiceId: string(edge.Node.Id), - Aliases: edge.Node.Aliases, - Locked: edge.Locked, - Notes: edge.Notes, + Id: string(edge.Id), + ComponentId: string(edge.Node.Id), + Aliases: edge.Node.Aliases, + Notes: edge.Notes, } dependencies = append(dependencies, dep) } @@ -838,7 +833,6 @@ For complete reference: "componentDependents", mcp.WithDescription("Get all the services that depend on a specific component. Returns the reverse dependency graph showing which services consume or call this component."), mcp.WithString("serviceId", mcp.Required(), mcp.Description("The id of the service to fetch dependents for.")), - mcp.WithString("search", mcp.Description("Optional search term to filter dependents by name.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: "Component Dependents in OpsLevel", ReadOnlyHint: &trueValue, @@ -853,34 +847,31 @@ For complete reference: return mcp.NewToolResultError("serviceId parameter is required"), nil } - service := opslevel.Service{ - ServiceId: opslevel.ServiceId{ - Id: opslevel.ID(serviceId), - }, + service, err := client.GetService(serviceId) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get service", err), nil + } + if service.Id == "" { + return mcp.NewToolResultError(fmt.Sprintf("service with id %s not found", serviceId)), nil } - search := req.GetString("search", "") variables := &opslevel.PayloadVariables{ "after": "", "first": 100, } - if search != "" { - (*variables)["search"] = search - } resp, err := service.GetDependents(client, variables) if err != nil { return mcp.NewToolResultErrorFromErr("failed to get dependents", err), nil } - var dependents []serializedDependency + dependents := []serializedDependency{} for _, edge := range resp.Edges { dep := serializedDependency{ - Id: string(edge.Id), - ServiceId: string(edge.Node.Id), - Aliases: edge.Node.Aliases, - Locked: edge.Locked, - Notes: edge.Notes, + Id: string(edge.Id), + ComponentId: string(edge.Node.Id), + Aliases: edge.Node.Aliases, + Notes: edge.Notes, } dependents = append(dependents, dep) } From e483ef1512e06f60d7c8dc5804b344b58e723f8c Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Wed, 18 Feb 2026 09:28:06 -0500 Subject: [PATCH 3/5] Add changie entry for service dependency tools Co-Authored-By: Claude Sonnet 4.6 --- .changes/unreleased/Added-20260218-092748.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changes/unreleased/Added-20260218-092748.yaml diff --git a/.changes/unreleased/Added-20260218-092748.yaml b/.changes/unreleased/Added-20260218-092748.yaml new file mode 100644 index 0000000..64d408a --- /dev/null +++ b/.changes/unreleased/Added-20260218-092748.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Add componentDependencies and componentDependents tools to fetch the upstream and downstream service dependency graph for a given component +time: 2026-02-18T09:27:48.836718-05:00 From a6d7f0417358aa73edb84525b2ac24c10bcf319a Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Wed, 18 Feb 2026 11:00:28 -0500 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Farjaad <8631006+Farjaad@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0665188..f51201e 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ Currently, the MCP server only uses read-only access to your OpsLevel account an - Campaigns - Checks - Components -- Component Dependencies (services that a component depends on) -- Component Dependents (services that depend on a component) +- Component Dependencies (components that a component depends on) +- Component Dependents (components that depend on a component) - Documentation (API & Tech Docs) - Domains - Filters From 9e2bb84eb0b535c496dce0f64cd4b17e2265b18f Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Wed, 18 Feb 2026 13:31:16 -0500 Subject: [PATCH 5/5] rename all service references in new field to component --- src/cmd/root.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/cmd/root.go b/src/cmd/root.go index e1a6c87..2f185d1 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -779,8 +779,8 @@ For complete reference: s.AddTool( mcp.NewTool( "componentDependencies", - mcp.WithDescription("Get all the services that a specific component depends on. Returns the dependency graph showing which services this component consumes or calls."), - mcp.WithString("serviceId", mcp.Required(), mcp.Description("The id of the service to fetch dependencies for.")), + mcp.WithDescription("Get all the components that a specific component depends on. Returns the dependency graph showing which components this component consumes or calls."), + mcp.WithString("componentId", mcp.Required(), mcp.Description("The id of the component to fetch dependencies for.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: "Component Dependencies in OpsLevel", ReadOnlyHint: &trueValue, @@ -790,17 +790,17 @@ For complete reference: }), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - serviceId, err := req.RequireString("serviceId") + componentId, err := req.RequireString("componentId") if err != nil { - return mcp.NewToolResultError("serviceId parameter is required"), nil + return mcp.NewToolResultError("componentId parameter is required"), nil } - service, err := client.GetService(serviceId) + service, err := client.GetService(componentId) if err != nil { - return mcp.NewToolResultErrorFromErr("failed to get service", err), nil + return mcp.NewToolResultErrorFromErr("failed to get component", err), nil } if service.Id == "" { - return mcp.NewToolResultError(fmt.Sprintf("service with id %s not found", serviceId)), nil + return mcp.NewToolResultError(fmt.Sprintf("component with id %s not found", componentId)), nil } variables := &opslevel.PayloadVariables{ @@ -831,8 +831,8 @@ For complete reference: s.AddTool( mcp.NewTool( "componentDependents", - mcp.WithDescription("Get all the services that depend on a specific component. Returns the reverse dependency graph showing which services consume or call this component."), - mcp.WithString("serviceId", mcp.Required(), mcp.Description("The id of the service to fetch dependents for.")), + mcp.WithDescription("Get all the components that depend on a specific component. Returns the reverse dependency graph showing which components consume or call this component."), + mcp.WithString("componentId", mcp.Required(), mcp.Description("The id of the component to fetch dependents for.")), mcp.WithToolAnnotation(mcp.ToolAnnotation{ Title: "Component Dependents in OpsLevel", ReadOnlyHint: &trueValue, @@ -842,17 +842,17 @@ For complete reference: }), ), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - serviceId, err := req.RequireString("serviceId") + componentId, err := req.RequireString("componentId") if err != nil { - return mcp.NewToolResultError("serviceId parameter is required"), nil + return mcp.NewToolResultError("componentId parameter is required"), nil } - service, err := client.GetService(serviceId) + service, err := client.GetService(componentId) if err != nil { - return mcp.NewToolResultErrorFromErr("failed to get service", err), nil + return mcp.NewToolResultErrorFromErr("failed to get component", err), nil } if service.Id == "" { - return mcp.NewToolResultError(fmt.Sprintf("service with id %s not found", serviceId)), nil + return mcp.NewToolResultError(fmt.Sprintf("component with id %s not found", componentId)), nil } variables := &opslevel.PayloadVariables{