diff --git a/src/MooseMCP-Tests/MMCPToolGetEntityTest.class.st b/src/MooseMCP-Tests/MMCPToolGetEntityTest.class.st index 27e6d90..20aa7a4 100644 --- a/src/MooseMCP-Tests/MMCPToolGetEntityTest.class.st +++ b/src/MooseMCP-Tests/MMCPToolGetEntityTest.class.st @@ -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 [ diff --git a/src/MooseMCP-Tests/MMCPToolGetMetricsTest.class.st b/src/MooseMCP-Tests/MMCPToolGetMetricsTest.class.st index 2245507..69f345c 100644 --- a/src/MooseMCP-Tests/MMCPToolGetMetricsTest.class.st +++ b/src/MooseMCP-Tests/MMCPToolGetMetricsTest.class.st @@ -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 [ @@ -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 [ diff --git a/src/MooseMCP-Tests/MMCPToolGetRelationshipsTest.class.st b/src/MooseMCP-Tests/MMCPToolGetRelationshipsTest.class.st index 390016f..6ddeea9 100644 --- a/src/MooseMCP-Tests/MMCPToolGetRelationshipsTest.class.st +++ b/src/MooseMCP-Tests/MMCPToolGetRelationshipsTest.class.st @@ -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 [ @@ -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 [ diff --git a/src/MooseMCP-Tests/MMCPToolQueryTest.class.st b/src/MooseMCP-Tests/MMCPToolQueryTest.class.st index 49011c7..262cadb 100644 --- a/src/MooseMCP-Tests/MMCPToolQueryTest.class.st +++ b/src/MooseMCP-Tests/MMCPToolQueryTest.class.st @@ -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 [ diff --git a/src/MooseMCP/MMCPCommand.class.st b/src/MooseMCP/MMCPCommand.class.st index 0bb83f5..183d788 100644 --- a/src/MooseMCP/MMCPCommand.class.st +++ b/src/MooseMCP/MMCPCommand.class.st @@ -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 ]. @@ -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 [ diff --git a/src/MooseMCP/MMCPGetEntityCommand.class.st b/src/MooseMCP/MMCPGetEntityCommand.class.st index a9bce3a..0106090 100644 --- a/src/MooseMCP/MMCPGetEntityCommand.class.st +++ b/src/MooseMCP/MMCPGetEntityCommand.class.st @@ -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: [ diff --git a/src/MooseMCP/MMCPGetMetricsCommand.class.st b/src/MooseMCP/MMCPGetMetricsCommand.class.st index 8aec02a..9b2d0da 100644 --- a/src/MooseMCP/MMCPGetMetricsCommand.class.st +++ b/src/MooseMCP/MMCPGetMetricsCommand.class.st @@ -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: [ diff --git a/src/MooseMCP/MMCPGetRelationshipsCommand.class.st b/src/MooseMCP/MMCPGetRelationshipsCommand.class.st index 9f275bc..bf62267 100644 --- a/src/MooseMCP/MMCPGetRelationshipsCommand.class.st +++ b/src/MooseMCP/MMCPGetRelationshipsCommand.class.st @@ -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. @@ -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. diff --git a/src/MooseMCP/MMCPQueryCommand.class.st b/src/MooseMCP/MMCPQueryCommand.class.st index 0eefbcc..1554ba5 100644 --- a/src/MooseMCP/MMCPQueryCommand.class.st +++ b/src/MooseMCP/MMCPQueryCommand.class.st @@ -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 ] diff --git a/src/MooseMCP/MMCPToolGetEntity.class.st b/src/MooseMCP/MMCPToolGetEntity.class.st index 9875ed8..b52a3bf 100644 --- a/src/MooseMCP/MMCPToolGetEntity.class.st +++ b/src/MooseMCP/MMCPToolGetEntity.class.st @@ -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: diff --git a/src/MooseMCP/MMCPToolGetMetrics.class.st b/src/MooseMCP/MMCPToolGetMetrics.class.st index 850bb0a..65bb351 100644 --- a/src/MooseMCP/MMCPToolGetMetrics.class.st +++ b/src/MooseMCP/MMCPToolGetMetrics.class.st @@ -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.' @@ -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 [ diff --git a/src/MooseMCP/MMCPToolGetRelationships.class.st b/src/MooseMCP/MMCPToolGetRelationships.class.st index 56c919f..29585ac 100644 --- a/src/MooseMCP/MMCPToolGetRelationships.class.st +++ b/src/MooseMCP/MMCPToolGetRelationships.class.st @@ -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).' @@ -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 [ diff --git a/src/MooseMCP/MMCPToolOnModel.class.st b/src/MooseMCP/MMCPToolOnModel.class.st index be72b01..fa966b7 100644 --- a/src/MooseMCP/MMCPToolOnModel.class.st +++ b/src/MooseMCP/MMCPToolOnModel.class.st @@ -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 [ diff --git a/src/MooseMCP/MMCPToolQuery.class.st b/src/MooseMCP/MMCPToolQuery.class.st index 8ee2762..5e9791a 100644 --- a/src/MooseMCP/MMCPToolQuery.class.st +++ b/src/MooseMCP/MMCPToolQuery.class.st @@ -24,10 +24,8 @@ MMCPToolQuery >> buildInputSchema [ ^ self objectInputSchemaWithProperties: { (self modelIdSchemaPropertyDescribed: 'The model to search in.'). - (self - stringEnumSchemaPropertyNamed: 'entityType' - description: 'Kind of entity to select.' - values: #( 'package' 'class' 'method' 'attribute' 'variable' )). + (self entityTypeSchemaPropertyDescribed: + 'Kind of entity to select: 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 filtersSchemaProperty } required: #( 'modelId' 'entityType' ) ]