From 81f091ddfd3068c2c824d5965515d714ed1d23b0 Mon Sep 17 00:00:00 2001 From: Gabriel Darbord <78592838+Gabriel-Darbord@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:36:11 +0200 Subject: [PATCH 1/2] Register inherited MCP handlers for subclasses --- .../MCPSubclassHandlerTestServer.class.st | 17 ++++++++++++ src/MCP-Tests/MCPToolContractsTest.class.st | 26 +++++++++++++++++++ src/MCP/MCPMessageProcessor.class.st | 10 +++++-- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/MCP-Tests-Resources/MCPSubclassHandlerTestServer.class.st diff --git a/src/MCP-Tests-Resources/MCPSubclassHandlerTestServer.class.st b/src/MCP-Tests-Resources/MCPSubclassHandlerTestServer.class.st new file mode 100644 index 0000000..761209a --- /dev/null +++ b/src/MCP-Tests-Resources/MCPSubclassHandlerTestServer.class.st @@ -0,0 +1,17 @@ +" +Test fixture MCP subclass used to verify inherited JSON-RPC handler registration and subclass handler overrides. +" +Class { + #name : 'MCPSubclassHandlerTestServer', + #superclass : 'MCP', + #category : 'MCP-Tests-Resources-Fixtures', + #package : 'MCP-Tests-Resources', + #tag : 'Fixtures' +} + +{ #category : 'jrpc' } +MCPSubclassHandlerTestServer >> rpcPing [ + + + ^ { (#source -> 'subclass') } asDictionary +] diff --git a/src/MCP-Tests/MCPToolContractsTest.class.st b/src/MCP-Tests/MCPToolContractsTest.class.st index b744d77..7abe458 100644 --- a/src/MCP-Tests/MCPToolContractsTest.class.st +++ b/src/MCP-Tests/MCPToolContractsTest.class.st @@ -1524,6 +1524,32 @@ MCPToolContractsTest >> testMCPUnregisterToolNamedRefreshesStaticTools [ self deny: (server staticToolNames includes: MCPHaltingTestTool toolName) ] +{ #category : 'tests' } +MCPToolContractsTest >> testMessageProcessorRegistersInheritedMCPHandlersForSubclass [ + + | processor request response server | + server := MCPSubclassHandlerTestServer new. + processor := MCPMessageProcessor new. + processor addHandlersFromPragmasIn: server. + request := JRPCRequestObject id: 1 method: 'tools/list' params: Dictionary new. + response := processor handleJRPCRequestObject: request. + self assert: response class equals: JRPCSuccessResponseObject +] + +{ #category : 'tests' } +MCPToolContractsTest >> testMessageProcessorUsesSubclassJRPCHandlerOverride [ + + | processor request response result server | + server := MCPSubclassHandlerTestServer new. + processor := MCPMessageProcessor new. + processor addHandlersFromPragmasIn: server. + request := JRPCRequestObject id: 1 method: 'ping' params: Dictionary new. + response := processor handleJRPCRequestObject: request. + result := response result. + self assert: response class equals: JRPCSuccessResponseObject. + self assert: (result at: #source) equals: 'subclass' +] + { #category : 'tests' } MCPToolContractsTest >> testMethodMetadataSearchDescriptionPointsToSpecializedReferenceTools [ diff --git a/src/MCP/MCPMessageProcessor.class.st b/src/MCP/MCPMessageProcessor.class.st index 05979d5..e7a30db 100644 --- a/src/MCP/MCPMessageProcessor.class.st +++ b/src/MCP/MCPMessageProcessor.class.st @@ -20,9 +20,9 @@ MCPMessageProcessor >> addHandler: aJRPCHandler [ ] { #category : 'handlers management' } -MCPMessageProcessor >> addHandlersFromPragmasIn: anObject [ +MCPMessageProcessor >> addHandlersFromPragmasFrom: aSubClass to: aSuperClass for: anObject [ - (Pragma allNamed: #jrpc: in: anObject class) do: [ :pragma | + (Pragma allNamed: #jrpc: from: aSubClass to: aSuperClass) do: [ :pragma | self addHandler: (MCPMessageSendHandler new receiver: anObject; methodName: pragma arguments first; @@ -30,6 +30,12 @@ MCPMessageProcessor >> addHandlersFromPragmasIn: anObject [ yourself) ] ] +{ #category : 'handlers management' } +MCPMessageProcessor >> addHandlersFromPragmasIn: anObject [ + + self addHandlersFromPragmasFrom: anObject class to: MCP for: anObject +] + { #category : 'handling - jrpc' } MCPMessageProcessor >> handleJRPCRequestObject: aJRPCRequestObject [ From 03180d408091289296937b64af915ef839acedd0 Mon Sep 17 00:00:00 2001 From: Gabriel Darbord <78592838+Gabriel-Darbord@users.noreply.github.com> Date: Mon, 31 Aug 2026 16:48:15 +0200 Subject: [PATCH 2/2] Advertise resources and prompts capabilities --- src/MCP-Tests/MCPToolContractsTest.class.st | 57 +++++++++++++++++++ src/MCP/MCP.class.st | 31 +++++++++- .../MCPStructurePromptsCapabilities.class.st | 36 ++++++++++++ ...MCPStructureResourcesCapabilities.class.st | 50 ++++++++++++++++ 4 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 src/MCP/MCPStructurePromptsCapabilities.class.st create mode 100644 src/MCP/MCPStructureResourcesCapabilities.class.st diff --git a/src/MCP-Tests/MCPToolContractsTest.class.st b/src/MCP-Tests/MCPToolContractsTest.class.st index 7abe458..f95b36e 100644 --- a/src/MCP-Tests/MCPToolContractsTest.class.st +++ b/src/MCP-Tests/MCPToolContractsTest.class.st @@ -1455,6 +1455,21 @@ MCPToolContractsTest >> testInitializeAdvertisesSupportedPharoVersions [ self deny: runtimeVersion isEmpty ] +{ #category : 'tests' } +MCPToolContractsTest >> testInitializeAdvertisesSupportedProtocolDomains [ + + | capabilities response | + response := (self mcpWithoutObservabilityExport + onInitialize: nil + capabilities: nil + clientInfo: nil + websiteUrl: nil) asJRPCJSON. + capabilities := response at: #capabilities. + self assert: (capabilities at: #tools) equals: Dictionary new. + self assert: (capabilities at: #resources) equals: Dictionary new. + self assert: (capabilities at: #prompts) equals: Dictionary new +] + { #category : 'tests' } MCPToolContractsTest >> testInitializeResponseSerializesSupportedPharoVersions [ @@ -1842,6 +1857,23 @@ MCPToolContractsTest >> testProjectMethodsDoNotSendReflectiveProtocolChecks [ (offendingMethods collect: [ :method | method printString ]) asArray printString }) ] +{ #category : 'tests' } +MCPToolContractsTest >> testPromptsGetReturnsInvalidParamsWhenPromptIsUnknown [ + + | response | + response := self handleJSON: '{"jsonrpc":"2.0","id":1,"method":"prompts/get","params":{"name":"missing"}}'. + self assert: (response includesSubstring: '"code":-32602'). + self assert: (response includesSubstring: '"message":"Invalid method parameter(s)."') +] + +{ #category : 'tests' } +MCPToolContractsTest >> testPromptsListReturnsEmptyPromptList [ + + | response | + response := self handleJSON: '{"jsonrpc":"2.0","id":1,"method":"prompts/list"}'. + self assert: (response includesSubstring: '"result":{"prompts":[]}') +] + { #category : 'tests' } MCPToolContractsTest >> testProtocolVersionNegotiation [ @@ -2034,6 +2066,31 @@ MCPToolContractsTest >> testRequestedToolIconsAreSelected [ equals: (MCPStructureIcon compositeBaseIconNamed: #pharoHalfScale badgeIconNamed: #go) asJRPCJSON ] +{ #category : 'tests' } +MCPToolContractsTest >> testResourcesListReturnsEmptyResourceList [ + + | response | + response := self handleJSON: '{"jsonrpc":"2.0","id":1,"method":"resources/list"}'. + self assert: (response includesSubstring: '"result":{"resources":[]}') +] + +{ #category : 'tests' } +MCPToolContractsTest >> testResourcesReadReturnsInvalidParamsWhenResourceIsUnknown [ + + | response | + response := self handleJSON: '{"jsonrpc":"2.0","id":1,"method":"resources/read","params":{"uri":"pharo://missing"}}'. + self assert: (response includesSubstring: '"code":-32602'). + self assert: (response includesSubstring: '"message":"Invalid method parameter(s)."') +] + +{ #category : 'tests' } +MCPToolContractsTest >> testResourcesTemplatesListReturnsEmptyTemplateList [ + + | response | + response := self handleJSON: '{"jsonrpc":"2.0","id":1,"method":"resources/templates/list"}'. + self assert: (response includesSubstring: '"result":{"resourceTemplates":[]}') +] + { #category : 'tests' } MCPToolContractsTest >> testRewriteMethodsIsMethodScopedCommandTool [ diff --git a/src/MCP/MCP.class.st b/src/MCP/MCP.class.st index 7241df3..9db32b4 100644 --- a/src/MCP/MCP.class.st +++ b/src/MCP/MCP.class.st @@ -326,7 +326,11 @@ MCP >> onInitialize: protocolVersion capabilities: capabilities clientInfo: clie ^ MCPStructureInitialize new protocolVersion: (self class negotiatedProtocolVersionFor: protocolVersion); - capabilities: (MCPStructureCapabilities new tools: MCPStructureToolsCapabilities new); + capabilities: (MCPStructureCapabilities new + tools: MCPStructureToolsCapabilities new; + resources: MCPStructureResourcesCapabilities new; + prompts: MCPStructurePromptsCapabilities new; + yourself); metadata: self serverInfrastructureMetadata; serverInfo: (MCPStructureServerInfo new name: 'MCP Pharo'; @@ -430,18 +434,39 @@ MCP >> rpcPing [ ^ Dictionary new ] +{ #category : 'rpc' } +MCP >> rpcPromptGet: name arguments: arguments [ + + + JRPCInvalidParameters signal +] + +{ #category : 'rpc' } +MCP >> rpcPromptsList [ + + + ^ { (#prompts -> #( )) } asDictionary +] + +{ #category : 'rpc' } +MCP >> rpcResourceRead: uri [ + + + JRPCInvalidParameters signal +] + { #category : 'rpc' } MCP >> rpcResourcesList [ - ^ { (#resource -> { }) } asDictionary + ^ { (#resources -> #( )) } asDictionary ] { #category : 'rpc' } MCP >> rpcResourcesTemplateList [ - ^ { (#resourceTemplates -> { }) } asDictionary + ^ { (#resourceTemplates -> #( )) } asDictionary ] { #category : 'rpc' } diff --git a/src/MCP/MCPStructurePromptsCapabilities.class.st b/src/MCP/MCPStructurePromptsCapabilities.class.st new file mode 100644 index 0000000..8666076 --- /dev/null +++ b/src/MCP/MCPStructurePromptsCapabilities.class.st @@ -0,0 +1,36 @@ +" +MCP protocol structure for the prompts capability. + +It records optional prompts capability flags advertised during initialization. +" +Class { + #name : 'MCPStructurePromptsCapabilities', + #superclass : 'Object', + #instVars : [ + 'listChanged' + ], + #category : 'MCP-Structure', + #package : 'MCP', + #tag : 'Structure' +} + +{ #category : 'accessing' } +MCPStructurePromptsCapabilities >> asJRPCJSON [ + + | dictionary | + dictionary := Dictionary new. + self listChanged ifNotNil: [ dictionary at: #listChanged put: self listChanged asJRPCJSON ]. + ^ dictionary +] + +{ #category : 'accessing' } +MCPStructurePromptsCapabilities >> listChanged [ + + ^ listChanged +] + +{ #category : 'accessing' } +MCPStructurePromptsCapabilities >> listChanged: anObject [ + + listChanged := anObject +] diff --git a/src/MCP/MCPStructureResourcesCapabilities.class.st b/src/MCP/MCPStructureResourcesCapabilities.class.st new file mode 100644 index 0000000..433f2e1 --- /dev/null +++ b/src/MCP/MCPStructureResourcesCapabilities.class.st @@ -0,0 +1,50 @@ +" +MCP protocol structure for the resources capability. + +It records optional resources capability flags advertised during initialization. +" +Class { + #name : 'MCPStructureResourcesCapabilities', + #superclass : 'Object', + #instVars : [ + 'subscribe', + 'listChanged' + ], + #category : 'MCP-Structure', + #package : 'MCP', + #tag : 'Structure' +} + +{ #category : 'accessing' } +MCPStructureResourcesCapabilities >> asJRPCJSON [ + + | dictionary | + dictionary := Dictionary new. + self listChanged ifNotNil: [ dictionary at: #listChanged put: self listChanged asJRPCJSON ]. + self subscribe ifNotNil: [ dictionary at: #subscribe put: self subscribe asJRPCJSON ]. + ^ dictionary +] + +{ #category : 'accessing' } +MCPStructureResourcesCapabilities >> listChanged [ + + ^ listChanged +] + +{ #category : 'accessing' } +MCPStructureResourcesCapabilities >> listChanged: anObject [ + + listChanged := anObject +] + +{ #category : 'accessing' } +MCPStructureResourcesCapabilities >> subscribe [ + + ^ subscribe +] + +{ #category : 'accessing' } +MCPStructureResourcesCapabilities >> subscribe: anObject [ + + subscribe := anObject +]