Skip to content

feat: CP-955 migrate ProjectErrorsTool #3688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 23, 2025
Merged

Conversation

samueld-activator
Copy link
Contributor

No description provided.

@samueld-activator samueld-activator requested a review from Naatan July 17, 2025 16:47
@samueld-activator samueld-activator self-assigned this Jul 17, 2025
@samueld-activator samueld-activator requested a review from Naatan July 18, 2025 18:46
),
Handler: func(ctx context.Context, p *primer.Values, mcpRequest mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace, _ := mcpRequest.RequireString("namespace")
Copy link
Member

Choose a reason for hiding this comment

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

Don't throw away errors.

ns, err := project.ParseNamespace(namespace)
if err != nil {
return mcp.NewToolResultError(fmt.Errorf("invalid namespace format. Use 'owner/project' format: %w", err).Error()), nil
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return mcp.NewToolResultError(fmt.Errorf("invalid namespace format. Use 'owner/project' format: %w", err).Error()), nil
return mcp.NewToolResultError(fmt.Sprintf("Could not parse namespace: %s", errs.JoinMessage(err))), nil
  1. Don't make assumptions about what the underlying errors is, the error it returns should (and does) give the format error. But it doesn't necessarily have to be a format error.
  2. No point passing it through Errorf() if you're just going to grab the message at the end.
  3. Always use errs.JoinMessage(err) or equivalent to get all underlying errors. You don't know if the direct error you're receiving has the actual cause of the issue in it.

err = runner.Run()
if err != nil {
return mcp.NewToolResultError(fmt.Errorf("error executing GraphQL query: %v", err).Error()), nil
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return mcp.NewToolResultError(fmt.Errorf("error executing GraphQL query: %v", err).Error()), nil
return mcp.NewToolResultError(fmt.Sprintf("error executing GraphQL query: %s", errs.JoinMessage(err))), nil

)

type ProjectErrorsRunner struct {
primer *primer.Values
Copy link
Member

Choose a reason for hiding this comment

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

Please record the underlying values here. The primers use-case is passing large amount of context. This runner only needs some of that context, which it should specify with an interface.

See for example:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added output, auth and svcModel, which are coming from the primer

Comment on lines 70 to 74
Name: artifact.Name(),
Version: artifact.Version(),
Namespace: artifact.Ingredients[0].Namespace,
LogURL: artifact.LogURL,
WasFixed: artifact.Revision() < int(*ingredient.Revision),
Copy link
Member

Choose a reason for hiding this comment

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

This is called FailedIngredient but other than the namespace is talking about an artifact. We should clearly differentiate between the two. Ingredient names and Artifact names are not always direct mappings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed it to FailedArtifact

Comment on lines 63 to 67
ingredient, err := model.GetIngredientByNameAndVersion(
artifact.Ingredients[0].Namespace, artifact.Name(), artifact.Version(), nil, runner.primer.Auth())
if err != nil {
return fmt.Errorf("error searching ingredient: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

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

This might be slow if you have a lot of failing artifacts. Probably fine for a first iteration, but might make sense to use hasura here to query multiple ingredients at the same time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I thought about that too. For now it doesn't seem to bad. I will add a note so we don't forget about it.

Comment on lines 78 to 82
// Check if ingredients are failing due to missing dependencies.
err = CheckDependencyErrors(&failedIngredients)
if err != nil {
return fmt.Errorf("error checking dependency errors: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

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

I think you should pass this failedArtifacts. You're not relying on WasFixed which is the only piece of meta info that failedIngredients has that failedArtifacts doesn't.

That way you can call this earlier, before failedIngredients is set, and you're not waiting on remotely pulling in all relevant ingredients before you error out on an unrelated error.

I see this is adding context to the failedIngredients, but you could always have this return a map that you then use to associate it to ingredients further down.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

Comment on lines +96 to +97
var wg sync.WaitGroup
var mu sync.Mutex
Copy link
Member

Choose a reason for hiding this comment

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

Ah, now you're getting to the good stuff! ;)

return
}
ingredient.IsDependencyError = depError
}(&(*failedIngredients)[i])
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I follow why this is creating a new pointer? Could conceivably just be

Suggested change
}(&(*failedIngredients)[i])
}(failedIngredients[i])

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I get "invalid operation: cannot index failedArtifacts (variable of type *[]FailedArtifact)"

failedArtifacts is a pointer, and this is why I can't index it. I need to dereference using *, but I can do that before "range artifacts" and use &artifacts[i] here

Comment on lines 126 to 166
var MissingDependencyKeywords = map[string][]string{
"language/php": {
"require_once(): Failed opening required",
"include(): Failed opening",
"require(): Failed opening required",
"include_once(): Failed opening",
"Fatal error: Uncaught Error: Class",
"Class not found",
},
"language/perl": {
"Can't locate",
"in @INC",
"BEGIN failed--compilation aborted",
},
"language/python": {
"ModuleNotFoundError",
"No module named",
"ImportError",
},
"language/tcl": {
"can't find package",
"couldn't load library",
"package require",
},
"language/ruby": {
"LoadError",
"cannot load such file",
"no such file to load",
"in `require'",
},
"language/c-sharp": {
"CS0246",
"are you missing a using directive",
"could not be found",
"The type or namespace",
},
"shared": {
"unresolved import", "no external crate", "E0432", "E0463", "can't find crate", // RUST
"No such file or directory", "fatal error:", "undefined reference to", // C/CPP
},
}
Copy link
Member

Choose a reason for hiding this comment

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

Many of these are far too vague to directly map to missing dependency errors.

I'd strongly prefer if this tool tried to find the most relevant parts of the log, rather than make any assertions about what the log is saying. The LLM will be MUCH better suited for this task than us keyword matching a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I rolled it back to check for ModuleNotFoundError as in its Python implementation.
For now, it's known to only work for Python. I added comments saying that we need to improve it to consider other languages and use a more robust mechanism

@samueld-activator samueld-activator requested a review from Naatan July 21, 2025 19:36
Copy link
Member

@Naatan Naatan left a comment

Choose a reason for hiding this comment

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

Thanks for sticking with me, nice work!

Comment on lines +68 to +78
type ArtifactOutput struct {
Name string `json:"name"`
Version string `json:"version"`
Namespace string `json:"namespace"`
IsBuildtimeDependency bool `json:"isBuildtimeDependency"`
IsRuntimeDependency bool `json:"isRuntimeDependency"`
LogURL string `json:"logURL"`
SourceURI string `json:"sourceURI"`
WasFixed bool `json:"wasFixed"`
IsDependencyError bool `json:"isDependencyError"`
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Define types at the top level (outside of the function) so they can be referenced outside of the function, and to keep the function low on boilerplate.

Comment on lines +79 to +94
for _, artifact := range failedArtifacts {
jsonBytes, err := json.Marshal(ArtifactOutput{
Name: artifact.Name(),
Version: artifact.Version(),
Namespace: artifact.Ingredients[0].Namespace,
IsBuildtimeDependency: artifact.IsBuildtimeDependency,
IsRuntimeDependency: artifact.IsRuntimeDependency,
LogURL: artifact.LogURL,
SourceURI: artifact.Ingredients[0].IngredientSource.Url.String(),
WasFixed: wasFixed[artifact.ArtifactID],
IsDependencyError: isDependencyError[artifact.ArtifactID],
})
if err != nil {
return fmt.Errorf("error marshaling results: %w", err)
}
runner.output.Print(string(jsonBytes))
Copy link
Member

Choose a reason for hiding this comment

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

Not asking for changes, just noting; Our output package handles structured output like this. But it's based on us running in either JSON or "plain" mode. ie. it affects ALL output. Not sure what this means for MCP, cause with MCP it's kind of a case by case.

Just noting it for now for awareness. Probably an area of improvement for later.

@samueld-activator samueld-activator merged commit 1b6ba39 into master Jul 23, 2025
6 of 7 checks passed
@samueld-activator samueld-activator deleted the samueld/CP-955 branch July 23, 2025 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants