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
4 changes: 4 additions & 0 deletions .changes/unreleased/Added-20250722-140404.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kind: Added
body: Add a filter argument to components to vastly decrease token and context usage
while improving success of reporting-style queries.
time: 2025-07-22T14:04:04.510339-07:00
95 changes: 93 additions & 2 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ func AllAccountMetadataStrings() []string {
return result
}

// componentFilter represents the filter structure for the components tool
type componentFilter struct {
Key string `json:"key,omitempty"`
Type string `json:"type,omitempty"`
Arg string `json:"arg,omitempty"`
Connective string `json:"connective,omitempty"`
Predicates []componentFilter `json:"predicates,omitempty"`
}

// newToolResult creates a CallToolResult for the passed object handling any json marshaling errors
func newToolResult(obj any, err error) (*mcp.CallToolResult, error) {
if err != nil {
Expand Down Expand Up @@ -250,7 +259,31 @@ var rootCmd = &cobra.Command{
s.AddTool(
mcp.NewTool(
"components",
mcp.WithDescription("Get all the components in the OpsLevel account. Components are objects in OpsLevel that represent things like apis, libraries, services, frontends, backends, etc. Use this tool to list what components are in the catalog, what team is the owner, what primary coding language is used, and what primary framework is used. It also includes its rubric level, corresponding to the maturity of the component; a higher index is better. A level is achieved by passing all checks tied to that same level. The Lifecycle field indicates the stage of the component (e.g., Alpha, Beta, GA, Decommissioned). The Tier field represents the importance and criticality of the component, with Tier 1 being the most critical (customer-facing with high impact) and Tier 4 being of least importance."),
mcp.WithDescription(`Filter and retrieve components in the OpsLevel catalog. Use as specific a filter as possible to narrow down results and avoid fetching a high number of components.

Components represent services, APIs, libraries, and other software artifacts with metadata such as owner (Team), language, framework, maturity level, lifecycle stage, and tier. Lower tier_index indicates greater criticality. Lower level_index indicates lower maturity level (e.g. Bronze=0, Silver=1, Gold=2).

Use the 'filter' parameter to narrow down results.
For simple filters:
{ "key": "name", "type": "equals", "arg": "service-name" }

For better precision, use composite filters:
{
"connective": "and",
"predicates": [
{ "key": "language", "type": "equals", "arg": "Python" },
{ "key": "owner_id", "type": "equals", "arg": "gid://opslevel/Team/123" }
]
}

Common filter keys: name, language, framework, owner_id, tags, tier_index, lifecycle_index
Common filter types: equals, contains, matches, exists, greater_than_or_equal_to

For complete reference:
- Keys: aliases, alert_status, component_type_id, creation_source, deploy_environment, domain_id, filter_id, framework, group_ids, language, level_index, lifecycle_index, name, owner_id, owner_ids, product, properties, property, relationship, repository_ids, system_id, tag, tags, tier_index
- Types: belongs_to, contains, does_not_contain, does_not_equal, does_not_exist, does_not_match, does_not_match_regex, ends_with, equals, exists, greater_than_or_equal_to, less_than_or_equal_to, matches, matches_regex, satisfies_jq_expression
`),
Copy link
Copy Markdown
Contributor Author

@derek-etherton-opslevel derek-etherton-opslevel Jul 22, 2025

Choose a reason for hiding this comment

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

Still can't find much in the way of concrete recommendations for tool description length, tool description vs argument description, etc. Having this detail in the top-level description should be better for planning, since most of these filters will require a bit of pre-work (fetching account metadata, team id, etc). Anywho this seems to work well and it's about as short + scannable as I can get it (yes I had AI go over it a few times).

Without a precise mapping from key <-> arg <-> predicate type, the agent will occasionally make a bad query, but the API errors are good enough that it can succeed on second shot. e.g. if it tries to query satisfies_jq_expression on a name, there will be an API error that states the supported predicate types. This is a worthy trade-off, because a complete mapping is brittle + a lot of context.

mcp.WithObject("filter", mcp.Description("Optional filter for components. For simple filters, provide {key, type, arg}. For composite filters, provide {connective, predicates}. See description for allowed values and format.")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: "Components in OpsLevel",
ReadOnlyHint: &trueValue,
Expand All @@ -260,7 +293,34 @@ var rootCmd = &cobra.Command{
}),
),
func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
resp, err := client.ListServices(nil)
var resp *opslevel.ServiceConnection
var err error
var filterInput *componentFilter

// Get the arguments map using the helper method
args := req.GetArguments()
if filterObj, exists := args["filter"]; exists && filterObj != nil {
// Marshal then unmarshal to our struct for type safety
filterBytes, marshalErr := json.Marshal(filterObj)
if marshalErr == nil {
var f componentFilter
if unmarshalErr := json.Unmarshal(filterBytes, &f); unmarshalErr == nil {
filterInput = &f
}
}
}

if filterInput != nil {
// Convert to ServiceFilterInput for the API
serviceFilter, convertErr := convertToServiceFilterInput(*filterInput)
if convertErr != nil {
return mcp.NewToolResultErrorFromErr("failed to convert filter", convertErr), nil
}
resp, err = client.ListServicesWithInputFilter(serviceFilter, nil)
} else {
resp, err = client.ListServices(nil)
}

if err != nil || resp == nil {
return mcp.NewToolResultErrorFromErr("failed to list components", err), nil
}
Expand Down Expand Up @@ -774,3 +834,34 @@ func getListDocumentPayloadVariables(searchTerm string) opslevel.PayloadVariable
"first": 100,
}
}

// convertToServiceFilterInput converts a componentFilter to a ServiceFilterInput for the OpsLevel API
func convertToServiceFilterInput(filter componentFilter) (opslevel.ServiceFilterInput, error) {
// Handle simple filter
if filter.Key != "" && filter.Type != "" {
return opslevel.ServiceFilterInput{
Key: opslevel.PredicateKeyEnum(filter.Key),
Arg: filter.Arg,
Type: opslevel.PredicateTypeEnum(filter.Type),
}, nil
}

// Handle composite filter
if filter.Connective != "" && len(filter.Predicates) > 0 {
var predInputs []opslevel.ServiceFilterInput
for _, p := range filter.Predicates {
predInput, err := convertToServiceFilterInput(p)
if err != nil {
return opslevel.ServiceFilterInput{}, err
}
predInputs = append(predInputs, predInput)
}
connective := opslevel.ConnectiveEnum(filter.Connective)
return opslevel.ServiceFilterInput{
Connective: &connective,
Predicates: &predInputs,
}, nil
}

return opslevel.ServiceFilterInput{}, fmt.Errorf("invalid filter format")
}
4 changes: 2 additions & 2 deletions src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.24.2

require (
github.com/mark3labs/mcp-go v0.34.0
github.com/opslevel/opslevel-go/v2025 v2025.5.28
github.com/opslevel/opslevel-go/v2025 v2025.7.28
github.com/relvacode/iso8601 v1.6.0
github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.9.1
Expand Down Expand Up @@ -55,4 +55,4 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/opslevel/opslevel-go/v2025 => ./submodules/opslevel-go
// replace github.com/opslevel/opslevel-go/v2025 => ./submodules/opslevel-go
2 changes: 2 additions & 0 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/opslevel/moredefaults v0.0.0-20240529152742-17d1318a3c12 h1:OQZ3W8kbyCcdS8QUWFTnZd6xtdkfhdckc7Paro7nXio=
github.com/opslevel/moredefaults v0.0.0-20240529152742-17d1318a3c12/go.mod h1:g2GSXVP6LO+5+AIsnMRPN+BeV86OXuFRTX7HXCDtYeI=
github.com/opslevel/opslevel-go/v2025 v2025.7.28 h1:TWqr0kmViigS3f8HR8C5wX7Or5aTodElmuzVMDMlmzk=
github.com/opslevel/opslevel-go/v2025 v2025.7.28/go.mod h1:Z2eSbXJ1Udn0gHm6z3Wi7a3sTMohtL5AlACl8YySoYs=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down