Skip to content

Commit caacabb

Browse files
committed
feat(mcp): add MCPService for handling MCP tool requests #330
Introduce MCPService to manage HTTP requests for MCP tools, including listing tools and executing them. The service integrates with the existing MCP tool infrastructure and supports JSON serialization for tool arguments and responses.
1 parent e970fbb commit caacabb

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

core/src/223/main/resources/META-INF/autodev-core.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
implementation="cc.unitmesh.devti.gui.snippet.error.CodeBlockHighlightingSettingsProvider"/>
7979
<daemon.intentionActionFilter
8080
implementation="cc.unitmesh.devti.gui.snippet.error.CodeBlockIntentionActionFilter"/>
81+
82+
<httpRequestHandler implementation="cc.unitmesh.devti.mcp.MCPService"/>
8183
</extensions>
8284

8385
<extensions defaultExtensionNs="JavaScript.JsonSchema">
@@ -221,6 +223,12 @@
221223
<extensionPoint qualifiedName="cc.unitmesh.knowledgeWebApiProvide"
222224
interface="cc.unitmesh.devti.bridge.provider.KnowledgeWebApiProvider"
223225
dynamic="true"/>
226+
227+
<!-- mcp -->
228+
<extensionPoint qualifiedName="cc.unitmesh.mcpTool"
229+
interface="cc.unitmesh.devti.mcp.McpTool"
230+
dynamic="true"/>
231+
224232
</extensionPoints>
225233

226234
<applicationListeners>

core/src/233/main/resources/META-INF/autodev-core.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777
implementation="cc.unitmesh.devti.gui.snippet.error.CodeBlockHighlightingSettingsProvider"/>
7878
<daemon.intentionActionFilter
7979
implementation="cc.unitmesh.devti.gui.snippet.error.CodeBlockIntentionActionFilter"/>
80+
81+
<httpRequestHandler implementation="cc.unitmesh.devti.mcp.MCPService"/>
8082
</extensions>
8183

8284
<extensions defaultExtensionNs="JavaScript.JsonSchema">
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2+
package cc.unitmesh.devti.mcp
3+
4+
import com.intellij.openapi.components.Service
5+
import com.intellij.openapi.components.service
6+
import com.intellij.openapi.diagnostic.logger
7+
import com.intellij.openapi.project.Project
8+
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream
9+
import io.netty.channel.ChannelHandlerContext
10+
import io.netty.handler.codec.http.FullHttpRequest
11+
import io.netty.handler.codec.http.HttpMethod
12+
import io.netty.handler.codec.http.QueryStringDecoder
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.serialization.builtins.ListSerializer
15+
import kotlinx.serialization.json.Json
16+
import kotlinx.serialization.serializer
17+
import org.jetbrains.ide.RestService
18+
import java.nio.charset.StandardCharsets
19+
import kotlin.reflect.KClass
20+
import kotlin.reflect.full.primaryConstructor
21+
import kotlin.reflect.full.starProjectedType
22+
23+
@Service
24+
class MCPUsageCollector(private val scope: CoroutineScope) {
25+
fun sendUsage(toolKey: String) {
26+
// todo
27+
}
28+
}
29+
30+
class MCPService : RestService() {
31+
private val serviceName = "mcp"
32+
private val json = Json {
33+
prettyPrint = true
34+
ignoreUnknownKeys = true
35+
classDiscriminator = "schemaType"
36+
}
37+
38+
override fun getServiceName(): String = serviceName
39+
40+
override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
41+
val path = urlDecoder.path().split(serviceName).last().trimStart('/')
42+
val project = getLastFocusedOrOpenedProject() ?: return null
43+
val tools = McpToolManager.Companion.getAllTools()
44+
45+
when (path) {
46+
"list_tools" -> handleListTools(tools, request, context)
47+
else -> handleToolExecution(path, tools, request, context, project)
48+
}
49+
return null
50+
}
51+
52+
private fun handleListTools(
53+
tools: List<AbstractMcpTool<*>>,
54+
request: FullHttpRequest,
55+
context: ChannelHandlerContext
56+
) {
57+
val toolsList = tools.map { tool ->
58+
ToolInfo(
59+
name = tool.name,
60+
description = tool.description,
61+
inputSchema = schemaFromDataClass(tool.argKlass)
62+
)
63+
}
64+
sendJson(toolsList, request, context)
65+
}
66+
67+
private fun handleToolExecution(
68+
path: String,
69+
tools: List<AbstractMcpTool<*>>,
70+
request: FullHttpRequest,
71+
context: ChannelHandlerContext,
72+
project: Project
73+
) {
74+
val tool = tools.find { it.name == path } ?: run {
75+
sendJson(Response(error = "Unknown tool: $path"), request, context)
76+
return
77+
}
78+
79+
service<MCPUsageCollector>().sendUsage(tool.name)
80+
val args = try {
81+
parseArgs(request, tool.argKlass)
82+
} catch (e: Throwable) {
83+
logger<MCPService>().warn("Failed to parse arguments for tool $path", e)
84+
sendJson(Response(error = e.message), request, context)
85+
return
86+
}
87+
val result = try {
88+
toolHandle(tool, project, args)
89+
} catch (e: Throwable) {
90+
logger<MCPService>().warn("Failed to execute tool $path", e)
91+
Response(error = "Failed to execute tool $path, message ${e.message}")
92+
}
93+
sendJson(result, request, context)
94+
}
95+
96+
@Suppress("UNCHECKED_CAST")
97+
private fun sendJson(data: Any, request: FullHttpRequest, context: ChannelHandlerContext) {
98+
val jsonString = when (data) {
99+
is List<*> -> json.encodeToString<List<ToolInfo>>(ListSerializer(ToolInfo.serializer()), data as List<ToolInfo>)
100+
is Response -> json.encodeToString<Response>(Response.serializer(), data)
101+
else -> throw IllegalArgumentException("Unsupported type for serialization")
102+
}
103+
val outputStream = BufferExposingByteArrayOutputStream()
104+
outputStream.write(jsonString.toByteArray(StandardCharsets.UTF_8))
105+
send(outputStream, request, context)
106+
}
107+
108+
@Suppress("UNCHECKED_CAST")
109+
private fun <T : Any> parseArgs(request: FullHttpRequest, klass: KClass<T>): T {
110+
val body = request.content().toString(StandardCharsets.UTF_8)
111+
if (body.isEmpty()) {
112+
return NoArgs as T
113+
}
114+
return when (klass) {
115+
NoArgs::class -> NoArgs as T
116+
else -> {
117+
json.decodeFromString(serializer(klass.starProjectedType), body) as T
118+
}
119+
}
120+
}
121+
122+
private fun <Args : Any> toolHandle(tool: McpTool<Args>, project: Project, args: Any): Response {
123+
@Suppress("UNCHECKED_CAST")
124+
return tool.handle(project, args as Args)
125+
}
126+
127+
override fun isMethodSupported(method: HttpMethod): Boolean =
128+
method === HttpMethod.GET || method === HttpMethod.POST
129+
130+
private fun schemaFromDataClass(kClass: KClass<*>): JsonSchemaObject {
131+
if (kClass == NoArgs::class) return JsonSchemaObject(type = "object")
132+
133+
val constructor = kClass.primaryConstructor
134+
?: error("Class ${kClass.simpleName} must have a primary constructor")
135+
136+
val properties = constructor.parameters.mapNotNull { param ->
137+
param.name?.let { name ->
138+
name to when (param.type.classifier) {
139+
String::class -> PropertySchema("string")
140+
Int::class, Long::class, Double::class, Float::class -> PropertySchema("number")
141+
Boolean::class -> PropertySchema("boolean")
142+
List::class -> PropertySchema("array")
143+
else -> PropertySchema("object")
144+
}
145+
}
146+
}.toMap()
147+
148+
val required = constructor.parameters
149+
.filter { !it.type.isMarkedNullable }
150+
.mapNotNull { it.name }
151+
152+
return JsonSchemaObject(
153+
type = "object",
154+
properties = properties,
155+
required = required
156+
)
157+
}
158+
}

0 commit comments

Comments
 (0)