Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
32222fa
Add MCP blackbox sampler — Phases 1–4
May 29, 2026
a1b9741
Wire MCP into EvoMaster entry point
May 30, 2026
7f75a39
Wrap McpSampler discovery in SutProblemException
May 30, 2026
ba9be0a
Handle HTTP 4xx from MCP servers that don't implement all endpoints
May 30, 2026
badf32d
Add missing header
Jun 2, 2026
cf1c967
Add MCP initialization handshake
Jun 5, 2026
af0cdd2
Merge branch 'master' into feature/mcp-blackbox-sampler
Jun 12, 2026
999c3e0
merge skeleton setup
Jun 12, 2026
5aea547
Fix for MCP uriTemplate
Jun 18, 2026
38ef921
store MCP-Session-Id on inititalize
Jun 19, 2026
56c3098
Setting up MCP Problem Type Skeleton (#3)
guidorc Jun 19, 2026
b4a4384
Merge branch 'master' into feature/mcp-blackbox-sampler
guidorc Jun 19, 2026
2b0e604
Adding JavaDocs
Jun 19, 2026
11df7b1
remove supress unchecked cast annotations
Jun 25, 2026
ef80412
Merge branch 'master' into feature/mcp-blackbox-sampler
guidorc Jul 4, 2026
e2820c6
Merge branch 'external-pr-mcp' into feature/mcp-blackbox-sampler
guidorc Jul 4, 2026
5cf695e
Merge branch 'master' into feature/mcp-blackbox-sampler
guidorc Jul 7, 2026
e210728
Merge branch 'master' into feature/mcp-blackbox-sampler
guidorc Jul 9, 2026
d240e23
Adopt Jersey client
Jul 13, 2026
c501ad1
Extract constants into shared file
Jul 13, 2026
ed9906d
use JsonNode data type for tools input schema
Jul 13, 2026
d4c0c34
use MediaType constants
guidorc Jul 13, 2026
4e6b786
remove redundant HTTP status check
guidorc Jul 13, 2026
14b9b8d
refactor MCP list response navigation
guidorc Jul 14, 2026
76948f5
Validate tool result includes type
guidorc Jul 14, 2026
9517441
general feedback
guidorc Jul 14, 2026
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
30 changes: 29 additions & 1 deletion core/src/main/kotlin/org/evomaster/core/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import org.evomaster.core.problem.externalservice.httpws.service.HttpWsExternalS
import org.evomaster.core.problem.graphql.GraphQLIndividual
import org.evomaster.core.problem.graphql.service.GraphQLBlackBoxModule
import org.evomaster.core.problem.graphql.service.GraphQLModule
import org.evomaster.core.problem.mcp.McpIndividual
import org.evomaster.core.problem.mcp.service.McpBlackBoxModule
import org.evomaster.core.problem.rest.data.RestIndividual
import org.evomaster.core.problem.rest.service.*
import org.evomaster.core.problem.rest.service.module.BlackBoxRestModule
Expand Down Expand Up @@ -615,7 +617,10 @@ class Main {
}

EMConfig.ProblemType.MCP -> {
throw IllegalStateException("MCP server analysis is not yet supported")
if (!config.blackBox) {
throw IllegalStateException("MCP only supports black-box mode")
}
McpBlackBoxModule(false)
}

//this should never happen, unless we add new type and forget to add it here
Expand Down Expand Up @@ -825,6 +830,28 @@ class Main {
}
}

private fun getAlgorithmKeyMcp(config: EMConfig): Key<out SearchAlgorithm<McpIndividual>> {

return when (config.algorithm) {
EMConfig.Algorithm.RANDOM ->
Key.get(object : TypeLiteral<RandomAlgorithm<McpIndividual>>() {})

EMConfig.Algorithm.MIO ->
Key.get(object : TypeLiteral<MioAlgorithm<McpIndividual>>() {})

EMConfig.Algorithm.MOSA ->
Key.get(object : TypeLiteral<MosaAlgorithm<McpIndividual>>() {})

EMConfig.Algorithm.WTS ->
Key.get(object : TypeLiteral<WtsAlgorithm<McpIndividual>>() {})

EMConfig.Algorithm.SMARTS ->
Key.get(object : TypeLiteral<SmartsAlgorithm<McpIndividual>>() {})

else -> throw IllegalStateException("Unrecognized algorithm ${config.algorithm} for MCP")
}
}

private fun getAlgorithmKeyRest(config: EMConfig): Key<out SearchAlgorithm<RestIndividual>> {

return when (config.algorithm) {
Expand Down Expand Up @@ -896,6 +923,7 @@ class Main {
EMConfig.ProblemType.GRAPHQL -> getAlgorithmKeyGraphQL(config)
EMConfig.ProblemType.RPC -> getAlgorithmKeyRPC(config)
EMConfig.ProblemType.WEBFRONTEND -> getAlgorithmKeyWeb(config)
EMConfig.ProblemType.MCP -> getAlgorithmKeyMcp(config)
else -> throw IllegalStateException("Unrecognized problem type ${config.problemType}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import org.evomaster.core.problem.api.param.Param
import org.evomaster.core.search.action.MainAction
import org.evomaster.core.search.gene.Gene

/**
* Base class for all actions that can be executed against an MCP server.
*
* An action can be either a tool call ([McpToolCallAction]) or a resource read ([McpResourceReadAction]).
*
* @param id stable identifier used as the key in the action cluster
* and as the coverage target name (e.g. "tool:myTool", "resource:file:///data")
* @param parameters the mutable action's inputs
*/
abstract class McpAction(
val id: String,
parameters: MutableList<Param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ package org.evomaster.core.problem.mcp

import org.evomaster.core.search.action.ActionResult

/**
* Stores the outcome of executing a single [McpAction] during fitness evaluation.
*
* A result is considered an error when the MCP server sets `isError: true` in the tool-call
* response, or when the fitness function catches an exception from the server.
*/
class McpCallResult : ActionResult {

companion object {
Expand All @@ -10,6 +16,10 @@ class McpCallResult : ActionResult {

constructor(sourceLocalId: String) : super(sourceLocalId)

/**
* Copy constructor. Delegates to the [ActionResult] copy constructor
* which propagates [stopping], [deathSentence], and the results map.
*/
private constructor(other: McpCallResult) : super(other)

override fun copy(): McpCallResult = McpCallResult(this)
Expand Down
20 changes: 20 additions & 0 deletions core/src/main/kotlin/org/evomaster/core/problem/mcp/McpConst.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.evomaster.core.problem.mcp

object McpConst {

/**
* JSON-RPC version used for all MCP messages, as required by the MCP specification.
*/
const val JSONRPC_VERSION = "2.0"

/**
* MCP protocol version negotiated during the initialize handshake.
*/
const val PROTOCOL_VERSION = "2025-11-25"

/**
* HTTP header used by the Streamable HTTP transport to carry the session id
* returned by the server after a successful initialize handshake.
*/
const val SESSION_ID_HEADER = "Mcp-Session-Id"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import org.evomaster.core.search.Individual
import org.evomaster.core.search.StructuralElement
import org.evomaster.core.search.action.ActionComponent

/**
* An individual (test case) for MCP blackbox testing.
*
* Represents a sequence of [McpAction]s — tool calls and resource reads — to be executed
* against an MCP server in order. Each action is wrapped in an [EnterpriseActionGroup] and
* stored in the MAIN group.
*
* The search engine samples, mutates, and evaluates instances of this class via
* [McpSampler] and [McpBlackBoxFitness] respectively.
*/
class McpIndividual(
sampleType: SampleType,
allActions: MutableList<out ActionComponent>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.evomaster.core.problem.mcp
import org.evomaster.core.problem.api.param.Param
import org.evomaster.core.search.gene.Gene

/** Parameter that carries the payload of a [McpToolCallAction]. */
class McpInputParam(name: String, gene: Gene) : Param(name, gene) {
override fun copyContent(): Param = McpInputParam(name, gene.copy())
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package org.evomaster.core.problem.mcp
import org.evomaster.core.search.action.Action

/**
* Action to read an MCP resource.
* For direct resources (isTemplate=false), the URI is fixed and no genes are mutable.
* For template resources (isTemplate=true), one StringGene per URI template variable
* (e.g. {city} in weather:///{city}/current) is created and fuzzed independently;
* call resolvedUri() to interpolate current gene values into the template.
* Action that reads a resource from an MCP server via the `resources/read` JSON-RPC method.
*
* @param uriTemplate the resource URI or URI template string (e.g. `file:///data`, `weather:///{city}/current`)
* @param uriParams one [McpUriParam] per template variable; empty for direct resources
* @param isTemplate whether this action represents a URI template or a fixed URI
*/
class McpResourceReadAction(
val uriTemplate: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ package org.evomaster.core.problem.mcp
import org.evomaster.core.search.action.Action
import org.evomaster.core.search.gene.ObjectGene

/**
* Action that invokes a named tool on an MCP server via the `tools/call` JSON-RPC method.
*
* @param toolName the name of the MCP tool to call
* @param inputSchema an [ObjectGene] which fields represent the tool's input arguments
*/
class McpToolCallAction(
val toolName: String,
val inputSchema: ObjectGene,
val description: String = ""
val inputSchema: ObjectGene
) : McpAction(
id = "tool:$toolName",
parameters = mutableListOf(McpInputParam("input", inputSchema))
) {
override fun copyContent(): Action {
return McpToolCallAction(toolName, inputSchema.copy() as ObjectGene, description)
return McpToolCallAction(toolName, inputSchema.copy() as ObjectGene)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.evomaster.core.problem.mcp
import org.evomaster.core.problem.api.param.Param
import org.evomaster.core.search.gene.Gene

/** Parameter representing a single URI template variable in a [McpResourceReadAction]. */
class McpUriParam(name: String, gene: Gene) : Param(name, gene) {
override fun copyContent(): Param = McpUriParam(name, gene.copy())
}
Loading