Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20260218-092748.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
111 changes: 111 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
Loading