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 diff --git a/README.md b/README.md index 4eda29a..f51201e 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 (components that a component depends on) +- Component Dependents (components 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..2f185d1 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -107,6 +107,13 @@ type serializedCampaign struct { Reminder *opslevel.CampaignReminder } +type serializedDependency struct { + Id string + ComponentId string + Aliases []string + Notes string +} + // AccountMetadata represents the different types of account metadata that can be fetched type AccountMetadata string @@ -768,6 +775,110 @@ For complete reference: return newToolResult(campaigns, err) }) + // Register component dependencies tool + s.AddTool( + mcp.NewTool( + "componentDependencies", + 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, + DestructiveHint: &falseValue, + IdempotentHint: &trueValue, + OpenWorldHint: &trueValue, + }), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + componentId, err := req.RequireString("componentId") + if err != nil { + return mcp.NewToolResultError("componentId parameter is required"), nil + } + + service, err := client.GetService(componentId) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get component", err), nil + } + if service.Id == "" { + return mcp.NewToolResultError(fmt.Sprintf("component with id %s not found", componentId)), nil + } + + variables := &opslevel.PayloadVariables{ + "after": "", + "first": 100, + } + + resp, err := service.GetDependencies(client, variables) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get dependencies", err), nil + } + + dependencies := []serializedDependency{} + for _, edge := range resp.Edges { + dep := serializedDependency{ + Id: string(edge.Id), + ComponentId: string(edge.Node.Id), + Aliases: edge.Node.Aliases, + 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 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, + DestructiveHint: &falseValue, + IdempotentHint: &trueValue, + OpenWorldHint: &trueValue, + }), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + componentId, err := req.RequireString("componentId") + if err != nil { + return mcp.NewToolResultError("componentId parameter is required"), nil + } + + service, err := client.GetService(componentId) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get component", err), nil + } + if service.Id == "" { + return mcp.NewToolResultError(fmt.Sprintf("component with id %s not found", componentId)), nil + } + + variables := &opslevel.PayloadVariables{ + "after": "", + "first": 100, + } + + resp, err := service.GetDependents(client, variables) + if err != nil { + return mcp.NewToolResultErrorFromErr("failed to get dependents", err), nil + } + + dependents := []serializedDependency{} + for _, edge := range resp.Edges { + dep := serializedDependency{ + Id: string(edge.Id), + ComponentId: string(edge.Node.Id), + Aliases: edge.Node.Aliases, + 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 {