Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 (services that a component depends on)
Comment thread
wesleyjellis marked this conversation as resolved.
Outdated
- Component Dependents (services that depend on a component)
Comment thread
wesleyjellis marked this conversation as resolved.
Outdated
- Documentation (API & Tech Docs)
- Domains
- Filters
Expand Down
120 changes: 120 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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),
Comment thread
wesleyjellis marked this conversation as resolved.
Outdated
Aliases: edge.Node.Aliases,
Locked: edge.Locked,
Notes: edge.Notes,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are kind of odd values to return. I think we should drop Locked, and I'm not sure what Notes contains, will have to look

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even Aliases is odd

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently that's just what's returned by dependencies.go. We get the information on the edge (like note, locked) and ServiceId, which is just Id, and Aliases[], no name. I don't really want to muck with the opslevel-go submodule for this so this is the best we can do for now

}
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 {
Expand Down