Skip to content

MCP support #17

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 17 commits into from
May 14, 2025
Merged

MCP support #17

merged 17 commits into from
May 14, 2025

Conversation

tiginamaria
Copy link
Collaborator

@tiginamaria tiginamaria commented May 8, 2025

MCP support:

Added McpToolRegistryProvider which instantiates mcp client (or gets as an argument), connects to it, get tools and convert to out tool format. The tool execute method calls mcp server with given args (provided in raw JsonObject as we can not build exact kotlin data class for them, see serialization)

Supported two types of connection stdio and sse, all for now in jvm module, but potentially sse can be multiplatform

@tiginamaria tiginamaria force-pushed the tigina/mcp-support branch from 1978c25 to 18a05fb Compare May 12, 2025 09:37
@tiginamaria tiginamaria marked this pull request as ready for review May 12, 2025 11:11
Copy link
Contributor

@EugeneTheDev EugeneTheDev left a comment

Choose a reason for hiding this comment

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

Overall looks nice, there are a few things I would like to discuss before merging: working with resources and tests

) : Tool<McpTool.Args, McpTool.Result>() {

companion object {
private val logger = LoggerFactory.create("ai.grazie.code.agents.mcp.McpTool")
Copy link
Contributor

Choose a reason for hiding this comment

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

Not used?

is JsonDecoder -> {
// Decode directly from JsonElement
val jsonElement = decoder.decodeJsonElement()
Args(jsonElement as JsonObject)
Copy link
Contributor

Choose a reason for hiding this comment

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

jsonElement.jsonObject is better

*/
override fun toStringDefault(): String {
if (promptMessageContents.isEmpty()) {
return "[No content]"
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe empty string is better?

val result = mcpClient.callTool(
name = descriptor.name, arguments = args.arguments
)
return Result(result?.content ?: emptyList())
Copy link
Contributor

Choose a reason for hiding this comment

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

content.orEmpty() is a bit more cleaner

// Create a ToolDescriptor
return ToolDescriptor(
name = sdkTool.name,
description = sdkTool.description ?: "",
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, .orEmpty()

* @property itemsType The type definition for the items within the array.
*/
data class List(val itemsType: ToolParameterType) : ToolParameterType("ARRAY")

/**
* Represents an array type parameter.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please fix doc comment, it represents and object, actually. But that's a good addition, we need to support objects

@@ -39,6 +35,19 @@ fun ToolDescriptor.toJSONSchema(): JsonObject {
put("type", "array")
put("items", toolParameterToSchema(type.itemsType))
}

is ToolParameterType.Object -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this whole function is not used anywhere, can we just drop it?

@@ -441,6 +441,20 @@ open class AnthropicDirectLLMClient(
"items" to getTypeMapForParameter(type.itemsType)
)
)

is ToolParameterType.Object -> JsonObject(
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to confirm, did you test that all providers support these nested types (objects) in their tool api?

* 3. Transforming MCP tools into the agent framework's Tool interface
* 4. Registering the transformed tools in a ToolRegistry
*/
class McpToolRegistryProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, I think we need tests for it, if it's possible

* 2. Calling the MCP tool through the MCP client
* 3. Converting MCP tool results back to agent framework tool results
*/
class McpTool(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be nice to add test for it too, if It's possible

@tiginamaria tiginamaria force-pushed the tigina/mcp-support branch from c0fdc58 to 84bf27a Compare May 13, 2025 14:28
@Rizzen
Copy link
Member

Rizzen commented May 13, 2025

I see that we're supporting using the mcp tools, but currently not supporting the agent being mcp server? Do we plan it/want it at all?

* @param element The JSON object representing the parameter type.
* @return The corresponding ToolParameterType, or null if the type cannot be parsed.
*/
private fun parseParameterType(element: JsonObject): ToolParameterType? {
Copy link
Member

Choose a reason for hiding this comment

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

I would move all the parsing into the separate class, just in case we (or our users) would want to change parsing logic

Copy link
Contributor

Choose a reason for hiding this comment

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

I also thought about somethign like this, but I'm not sure if this is the real case, since MCP format is kinda fixed, no? But giving more flexibility is still a good think, so I agree that we can think in this direction too

Copy link
Member

Choose a reason for hiding this comment

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

I mean it's just rule of thumb, MCP for now is fixed, but world is changing and we're not safe from MCPv2 someday

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, good points, as I already wrote, giving more flexibility and providing some sane default implementation can be nice

Copy link
Contributor

Choose a reason for hiding this comment

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

Created a separate parser interface with default implementation, where I moved this logic McpToolDescriptorParser

// Get the API key from environment variables
val openAIApiToken = System.getenv("OPEN_AI_TOKEN")

// Start the Docker container with the Google Maps MCP server
Copy link
Member

Choose a reason for hiding this comment

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

It's Playwright

).start()

// Wait for the server to start
Thread.sleep(2000)
Copy link
Member

Choose a reason for hiding this comment

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

Well, it's not very robust, because app start time heavily varies on user's machine and we could improve it here, for example waiting to socket to be open, but it's optional for now

Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is just in examples, let's leave it like this for now. We can add more complex logic later

Comment on lines 292 to 302
is ToolParameterType.Object -> {
put("type", JsonPrimitive("object"))
put("properties", buildJsonObject {
type.properties.forEach { property ->
put(property.name, buildJsonObject {
fillOpenRouterParamType(property.type)
put("description", property.description)
})
}
}
)
Copy link
Member

Choose a reason for hiding this comment

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

I suggest to move string literals to consts, here and all over OpenRouterDirectLLMClient

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's address this in a separate PR

Copy link
Contributor

@EugeneTheDev EugeneTheDev left a comment

Choose a reason for hiding this comment

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

We've improved API and added tests. Seems like in the current state we can merge it and work on additional features in the separate PRs

@EugeneTheDev EugeneTheDev merged commit cee6611 into main May 14, 2025
3 checks passed
@tiginamaria tiginamaria deleted the tigina/mcp-support branch May 21, 2025 16:09
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.

3 participants