Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions src/MooseMCP-Tests/MMCPToolGetEntityTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ MMCPToolGetEntityTest >> testFindsEntityByPartialName [
self assert: (entry at: 'name') equals: 'field1'
]

{ #category : 'tests' }
MMCPToolGetEntityTest >> testRaisesWhenEntityTypeUnknown [

self
should: [
self callToolWith: {
(#modelId -> self modelName).
(#query -> '').
(#entityType -> 'invocation') } asDictionary ]
raise: Error
]

{ #category : 'tests' }
MMCPToolGetEntityTest >> testRaisesWhenModelIdUnknown [

Expand Down
27 changes: 27 additions & 0 deletions src/MooseMCP-Tests/MMCPToolGetMetricsTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ MMCPToolGetMetricsTest >> testComputesDefaultMetricSetForEntity [
self assert: (metrics includesKey: 'DIT')
]

{ #category : 'tests' }
MMCPToolGetMetricsTest >> testComputesDiscoveredNumericProperty [
"weightedMethodCount is a real FM3 Number-typed property, not one of the curated
short-name aliases (LOC/NOM/WMC/...); it should still be computable by its own name."

| result metrics |
result := self callToolWith: {
(#modelId -> self modelName).
(#entityId -> fixture baseClass mooseID asString).
(#metrics -> #( 'weightedMethodCount' )) } asDictionary.
metrics := result anyOne at: 'metrics'.
self assert: (metrics includesKey: 'weightedMethodCount').
self assert: (metrics at: 'weightedMethodCount') isNumber
]

{ #category : 'tests' }
MMCPToolGetMetricsTest >> testExplicitMetricsListRestrictsOutput [

Expand Down Expand Up @@ -111,6 +126,18 @@ MMCPToolGetMetricsTest >> testRaisesWhenEntityIdUnknown [
raise: Error
]

{ #category : 'tests' }
MMCPToolGetMetricsTest >> testRaisesWhenMetricUnknown [

self
should: [
self callToolWith: {
(#modelId -> self modelName).
(#entityId -> fixture baseClass mooseID asString).
(#metrics -> #( 'bogus' )) } asDictionary ]
raise: Error
]

{ #category : 'private - calling' }
MMCPToolGetMetricsTest >> toolClass [

Expand Down
26 changes: 26 additions & 0 deletions src/MooseMCP-Tests/MMCPToolGetRelationshipsTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ MMCPToolGetRelationshipsTest >> testAccessRelationshipType [
equals: fixture attribute mooseID asString
]

{ #category : 'tests' }
MMCPToolGetRelationshipsTest >> testDefaultDirectionIsOutgoingOnly [
"baseMethod has no outgoing inheritance/invocation of its own; subMethod's invocation of
baseMethod is only visible from baseMethod's incoming side, which the default direction
(outgoing) should not include."

| result |
result := self callToolWith: {
(#modelId -> self modelName).
(#entityId -> fixture baseMethod mooseID asString) }
asDictionary.
self assert: (result at: 'edges') isEmpty
]

{ #category : 'tests' }
MMCPToolGetRelationshipsTest >> testDefaultTypesReturnInheritanceEdge [

Expand Down Expand Up @@ -140,6 +154,18 @@ MMCPToolGetRelationshipsTest >> testRaisesWhenEntityIdUnknown [
raise: Error
]

{ #category : 'tests' }
MMCPToolGetRelationshipsTest >> testRaisesWhenRelationshipTypeUnknown [

self
should: [
self callToolWith: {
(#modelId -> self modelName).
(#entityId -> fixture baseClass mooseID asString).
(#types -> #( 'bogus' )) } asDictionary ]
raise: Error
]

{ #category : 'private - calling' }
MMCPToolGetRelationshipsTest >> toolClass [

Expand Down
11 changes: 11 additions & 0 deletions src/MooseMCP-Tests/MMCPToolQueryTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ MMCPToolQueryTest >> testAppliesFiltersWhenSeeding [
((result at: 'preview') anyOne includesSubstring: 'BaseClass')
]

{ #category : 'tests' }
MMCPToolQueryTest >> testRaisesWhenEntityTypeUnknown [

self
should: [
self callToolWith: {
(#modelId -> self modelName).
(#entityType -> 'invocation') } asDictionary ]
raise: Error
]

{ #category : 'tests' }
MMCPToolQueryTest >> testSeedsSetFromEntityType [

Expand Down
78 changes: 77 additions & 1 deletion src/MooseMCP/MMCPCommand.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,63 @@ MMCPCommand >> asMermaid: edges [
s nextPut: Character lf ] ]
]

{ #category : 'model access' }
MMCPCommand >> associationTypeNamesIn: aModel [

^ aModel metamodel classes
select: [ :c |
c implementingClass allTraits includes: FamixTAssociation ]
thenCollect: #name
]

{ #category : 'model access' }
MMCPCommand >> checkEntityType: aTypeName in: aModel [

((self namedEntityTypeNamesIn: aModel) anySatisfy: [ :name |
name sameAs: aTypeName ]) ifFalse: [
MCPCommandError
signalErrorCode: #EntityTypeUnknown
message: 'Unknown entity type: ' , aTypeName asString , '.'
details: { (#entityType -> aTypeName) } asDictionary ]
]

{ #category : 'metrics' }
MMCPCommand >> checkMetrics: someMetricNames in: aModel [

| discoveredNames |
discoveredNames := self numericPropertyNamesIn: aModel.
someMetricNames do: [ :aMetricName |
((self metricSelectors includesKey: aMetricName) or: [
discoveredNames anySatisfy: [ :name | name sameAs: aMetricName ] ])
ifFalse: [
MCPCommandError
signalErrorCode: #MetricUnknown
message: 'Unknown metric: ' , aMetricName asString , '.'
details: { (#metric -> aMetricName) } asDictionary ] ]
]

{ #category : 'model access' }
MMCPCommand >> checkRelationshipTypes: someTypeNames in: aModel [

| validNames |
validNames := self associationTypeNamesIn: aModel.
someTypeNames do: [ :aTypeName |
(validNames anySatisfy: [ :name | name sameAs: aTypeName ])
ifFalse: [
MCPCommandError
signalErrorCode: #RelationshipTypeUnknown
message:
'Unknown relationship type: ' , aTypeName asString , '.'
details: { (#relationshipType -> aTypeName) } asDictionary ] ]
]

{ #category : 'metrics' }
MMCPCommand >> computeMetric: aMetricName for: anEntity [

| selector raw |
selector := self metricSelectors at: aMetricName ifAbsent: [ ^ nil ].
selector := self metricSelectors
at: aMetricName
ifAbsent: [ aMetricName asSymbol ].
raw := self safePerform: selector on: anEntity.
raw ifNil: [ ^ nil ].
(raw isKindOf: Collection) ifTrue: [ ^ raw size ].
Expand Down Expand Up @@ -182,6 +234,30 @@ MMCPCommand >> modelNamed: aModelId [
ifNone: [ Error signal: 'No model named ' , aModelId asString ]
]

{ #category : 'model access' }
MMCPCommand >> namedEntityTypeNamesIn: aModel [

^ aModel metamodel classes
select: [ :c |
c implementingClass allTraits includes: FamixTNamedEntity ]
thenCollect: #name
]

{ #category : 'metrics' }
MMCPCommand >> numericPropertyNamesIn: aModel [

| names |
names := Set new.
aModel metamodel classes
select: [ :c |
c implementingClass allTraits includes: FamixTNamedEntity ]
thenDo: [ :c |
c allProperties
select: [ :p | p type isKindOf: FM3Number ]
thenDo: [ :p | names add: p name ] ].
^ names
]

{ #category : 'relationships' }
MMCPCommand >> relatedTo: anEntity type: aType direction: aDirection [

Expand Down
4 changes: 3 additions & 1 deletion src/MooseMCP/MMCPGetEntityCommand.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ MMCPGetEntityCommand >> execute [
(self describeEntity: (self entityWithId: entityId in: model)) ].
candidates := (entityType isNil or: [ entityType isEmpty ])
ifTrue: [ model entityStorage ]
ifFalse: [ self entitiesOfType: entityType in: model ].
ifFalse: [
self checkEntityType: entityType in: model.
self entitiesOfType: entityType in: model ].
matches := (candidates select: [ :e |
| name |
name := (self safePerform: #mooseName on: e) ifNil: [
Expand Down
1 change: 1 addition & 0 deletions src/MooseMCP/MMCPGetMetricsCommand.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ MMCPGetMetricsCommand >> execute [
self metricSelectors keys asSortedCollection
asOrderedCollection ]
ifFalse: [ metrics ].
self checkMetrics: names in: model.
targets := (entityId isNil or: [ entityId isEmpty ])
ifTrue: [ self groupTargetsFor: model groupBy: groupBy ]
ifFalse: [
Expand Down
5 changes: 3 additions & 2 deletions src/MooseMCP/MMCPGetRelationshipsCommand.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ MMCPGetRelationshipsCommand >> execute [
default: #( 'inheritance' 'invocation' ).
direction := self request
stringArgumentNamed: 'direction'
default: 'both'.
default: 'outgoing'.
depth := self request
nonNegativeIntegerArgumentNamed: 'depth'
default: 1.
Expand All @@ -32,7 +32,8 @@ MMCPGetRelationshipsCommand >> execute [
effectiveTypes := (types isNil or: [ types isEmpty ])
ifTrue: [ #( 'inheritance' 'invocation' ) ]
ifFalse: [ types ].
dir := direction ifNil: [ 'both' ].
self checkRelationshipTypes: effectiveTypes in: model.
dir := direction ifNil: [ 'outgoing' ].
edges := OrderedCollection new.
visited := Set new.
visited add: root.
Expand Down
1 change: 1 addition & 0 deletions src/MooseMCP/MMCPQueryCommand.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ MMCPQueryCommand >> execute [
arrayArgumentNamed: 'filters'
ifAbsent: [ #( ) ].
model := self modelNamed: modelId.
self checkEntityType: entityType in: model.
candidates := self entitiesOfType: entityType in: model.
filtered := filters isEmpty
ifTrue: [ candidates ]
Expand Down
9 changes: 4 additions & 5 deletions src/MooseMCP/MMCPToolGetEntity.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ MMCPToolGetEntity >> buildInputSchema [
type: 'string'
description:
'Qualified name, simple name, or substring pattern.').
(self entityIdSchemaPropertyDescribed: 'Exact Moose entity id; if given, query is ignored.').
(self
stringEnumSchemaPropertyNamed: 'entityType'
description: 'Filter by kind.'
values: #( 'package' 'class' 'method' 'attribute' 'variable' )).
(self entityIdSchemaPropertyDescribed:
'Exact Moose entity id; if given, query is ignored.').
(self entityTypeSchemaPropertyDescribed:
'Filter by kind: a Famix named-entity type for this model (e.g. Class, Method, Package, Attribute; case-insensitive). Valid names depend on the model''s metamodel.').
(self
booleanSchemaPropertyNamed: 'exactMatch'
description:
Expand Down
32 changes: 23 additions & 9 deletions src/MooseMCP/MMCPToolGetMetrics.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,11 @@ MMCPToolGetMetrics >> buildInputSchema [

^ self
objectInputSchemaWithProperties: {
(self modelIdSchemaPropertyDescribed: 'The model to compute metrics in.').
(self entityIdSchemaPropertyDescribed: 'Entity to scope metrics to; omit to scope to the whole model.').
(self
stringEnumArraySchemaNamed: 'metrics'
description: 'Metrics to compute; omit for a sensible default set.'
itemDescription: 'A metric name.'
values:
#( 'LOC' 'NOM' 'WMC' 'cyclomaticComplexity'
'fanIn' 'fanOut' 'numberOfChildren' 'DIT' )).
(self modelIdSchemaPropertyDescribed:
'The model to compute metrics in.').
(self entityIdSchemaPropertyDescribed:
'Entity to scope metrics to; omit to scope to the whole model.').
self metricsSchemaProperty.
(self
stringEnumSchemaPropertyNamed: 'groupBy'
description: 'How to group metric rows.'
Expand All @@ -46,6 +42,24 @@ MMCPToolGetMetrics >> description [
^ 'Compute software metrics for an entity, or grouped across a whole Moose model.'
]

{ #category : 'private - schema' }
MMCPToolGetMetrics >> metricsSchemaProperty [

| itemsProperty |
itemsProperty := MCPStructureProperties new
type: 'string';
description:
'A metric name: one of the curated short names (LOC, NOM, WMC, cyclomaticComplexity, fanIn, fanOut, numberOfChildren, DIT) or any Number-typed property discoverable on this model''s named-entity metamodel classes (e.g. weightedMethodCount, numberOfPublicMethods, lcom2; case-insensitive).';
yourself.
^ (self
schemaPropertyNamed: 'metrics'
type: 'array'
description:
'Metrics to compute; omit for a sensible default set.')
items: itemsProperty;
yourself
]

{ #category : 'private - results' }
MMCPToolGetMetrics >> successSummaryForResult: result request: aRequest [

Expand Down
33 changes: 24 additions & 9 deletions src/MooseMCP/MMCPToolGetRelationships.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,16 @@ MMCPToolGetRelationships >> buildInputSchema [

^ self
objectInputSchemaWithProperties: {
(self modelIdSchemaPropertyDescribed: 'The model containing the entity.').
(self entityIdSchemaPropertyDescribed: 'The entity to get relationships for.').
(self
stringEnumArraySchemaNamed: 'types'
description: 'Relationship types to include.'
itemDescription: 'A relationship type.'
values:
#( 'inheritance' 'invocation' 'reference' 'access' 'association' )).
(self modelIdSchemaPropertyDescribed:
'The model containing the entity.').
(self entityIdSchemaPropertyDescribed:
'The entity to get relationships for.').
self relationshipTypesSchemaProperty.
(self
stringEnumSchemaPropertyNamed: 'direction'
description: 'Which direction of relationship to include.'
values: #( 'incoming' 'outgoing' 'both' )
default: 'both').
default: 'outgoing').
(self
integerSchemaPropertyNamed: 'depth'
description: 'Transitive closure depth (1 = direct only).'
Expand All @@ -53,6 +50,24 @@ MMCPToolGetRelationships >> description [
^ 'Get relationships (inheritance, calls, references, accesses) for an entity in a Moose model.'
]

{ #category : 'private - schema' }
MMCPToolGetRelationships >> relationshipTypesSchemaProperty [

| itemsProperty |
itemsProperty := MCPStructureProperties new
type: 'string';
description:
'A Famix association type for this model (e.g. Inheritance, Invocation, Access, Reference; case-insensitive). Valid names depend on the model''s metamodel.';
yourself.
^ (self
schemaPropertyNamed: 'types'
type: 'array'
description:
'Relationship types to include; omit for a sensible default set.')
items: itemsProperty;
yourself
]

{ #category : 'private - results' }
MMCPToolGetRelationships >> successSummaryForResult: result request: aRequest [

Expand Down
9 changes: 9 additions & 0 deletions src/MooseMCP/MMCPToolOnModel.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ MMCPToolOnModel >> entityIdSchemaPropertyDescribed: aDescription [
description: aDescription
]

{ #category : 'private - schema' }
MMCPToolOnModel >> entityTypeSchemaPropertyDescribed: aDescription [

^ self
schemaPropertyNamed: 'entityType'
type: 'string'
description: aDescription
]

{ #category : 'private - schema' }
MMCPToolOnModel >> modelIdSchemaPropertyDescribed: aDescription [

Expand Down
Loading