@evref-bl/pharo-mcp 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,22 @@ Metacello new
32
32
  load.
33
33
  ```
34
34
 
35
+ The baseline groups are:
36
+
37
+ - `Core`: the server, its transport and schema machinery, and the catalog
38
+ tools it dispatches through
39
+ - `IDE`: the image tools — classes, methods, packages, tests, critiques,
40
+ repositories, change history, debugging
41
+ - `UI`: the world-menu server list
42
+ - `Telemetry`: the OpenTelemetry backend
43
+ - `default`: `Core`, `IDE`, `UI` and their tests
44
+
45
+ A project embedding the server and serving tools of its own loads `Core` alone,
46
+ which leaves out the image tools and everything behind them. Loading its package
47
+ is all its tools need: a tool that names itself is in the catalog.
48
+ [Building on MCP](templates/pharo-references/building-on-mcp.md) walks through
49
+ a tool, a resource and a prompt of your own.
50
+
35
51
  Start the Streamable HTTP server:
36
52
 
37
53
  ```smalltalk
@@ -51,6 +67,7 @@ and first calls.
51
67
  - [Documentation index](docs/README.md)
52
68
  - [Getting started](templates/pharo-references/getting-started.md)
53
69
  - [Using MCP from an agent](templates/pharo-references/using-pharo-mcp.md)
70
+ - [Building on MCP](templates/pharo-references/building-on-mcp.md)
54
71
  - [Tool reference](templates/pharo-references/tool-reference.md)
55
72
  - [Safety and ecosystem integration](templates/pharo-references/safety-and-ecosystem.md)
56
73
  - [Troubleshooting](templates/pharo-references/troubleshooting.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evref-bl/pharo-mcp",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "description": "Pharo MCP agent-host templates",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,473 @@
1
+ # Building On MCP
2
+
3
+ MCP is a server you can embed. Loading `Core` gives you the Streamable HTTP
4
+ transport, the JSON-RPC and schema machinery and the catalog tools, and
5
+ nothing that knows about Pharo code. Everything the server then offers a
6
+ client — tools, resources, prompts — you write in a package of your own.
7
+
8
+ A project doing this changes nothing in MCP. Its tools live in a package of
9
+ its own, which it loads when it wants them served and leaves out when it does
10
+ not.
11
+
12
+ ## Load the server alone
13
+
14
+ Depend on the baseline and ask for `Core`:
15
+
16
+ ```smalltalk
17
+ BaselineOfMyProject >> baseline: spec
18
+
19
+ <baseline>
20
+ spec for: #common do: [
21
+ spec baseline: 'MCP' with: [
22
+ spec
23
+ repository: 'github://Evref-BL/MCP:vX.Y.Z/src'; "a release, not a branch"
24
+ loads: #( 'Core' ) ].
25
+ spec package: 'MyProject-MCP' with: [ spec requires: #( 'MyProject' 'MCP' ) ].
26
+ spec group: 'Tools' with: #( 'MyProject-MCP' ).
27
+ spec group: 'default' with: #( 'Core' 'Tests' ) ]
28
+ ```
29
+
30
+ Two things in it are worth copying. The group is named `Tools` rather than
31
+ `MCP`, because Metacello refuses a group and a baseline that share one name.
32
+ And it is left out of `default`: your project is useful without a server, and
33
+ an image that does not serve MCP has no use for tools it cannot register.
34
+
35
+ The other groups are `IDE`, MCP's own image tools for classes, methods,
36
+ packages, tests, repositories, change history and debugging; `UI`,
37
+ the world-menu server list; `Telemetry`, the OpenTelemetry backend; and
38
+ `default`, which is `Core`, `IDE`, `UI` and their tests.
39
+
40
+ ## The three things a server offers
41
+
42
+ | Kind | What it is | Who reaches for it | Subclass of | Names itself with |
43
+ | --- | --- | --- | --- | --- |
44
+ | Tool | an action with arguments | the model, on its own | `MCPTool` | `toolName` |
45
+ | Resource | a document to read | the host, for the user | `MCPResource` | `resourceUri` |
46
+ | Prompt | a message template | the user, deliberately | `MCPPrompt` | `promptName` |
47
+
48
+ They are independent. A server may have one of them, two, or all three.
49
+
50
+ ## A tool
51
+
52
+ Give your tools a common superclass. It is where the things they share live —
53
+ the group they belong to, the icon they are drawn with, the helpers that talk
54
+ to your project:
55
+
56
+ ```smalltalk
57
+ MCPTool << #MyProjectTool
58
+ package: 'MyProject-MCP'
59
+ ```
60
+
61
+ ```smalltalk
62
+ MyProjectTool class >> groupName
63
+
64
+ ^ 'widgets'
65
+ ```
66
+
67
+ The base is not served, and needs to say nothing to stay out: it answers no
68
+ `toolName`, so there is no tool there to register.
69
+
70
+ Then one class per tool:
71
+
72
+ ```smalltalk
73
+ MyProjectTool << #MyProjectToolListWidgets
74
+ package: 'MyProject-MCP'
75
+ ```
76
+
77
+ ```smalltalk
78
+ MyProjectToolListWidgets class >> toolName
79
+
80
+ ^ 'widget_list'
81
+
82
+ MyProjectToolListWidgets >> title
83
+
84
+ ^ 'List Widgets'
85
+
86
+ MyProjectToolListWidgets >> description
87
+
88
+ ^ 'List the widgets this image holds, optionally those whose name starts with a prefix. Use widget_get to read one.'
89
+
90
+ MyProjectToolListWidgets >> additionalKeywords
91
+
92
+ ^ #( 'widget' 'inventory' )
93
+
94
+ MyProjectToolListWidgets >> annotations
95
+
96
+ ^ MCPStructureToolAnnotations readOnly
97
+
98
+ MyProjectToolListWidgets >> buildInputSchema
99
+
100
+ ^ MCPStructureInputSchema new
101
+ type: 'object';
102
+ properties: { (self
103
+ schemaPropertyNamed: 'prefix'
104
+ type: 'string'
105
+ description: 'Only answer widgets whose name starts with this. Defaults to all of them.') };
106
+ required: #( );
107
+ yourself
108
+
109
+ MyProjectToolListWidgets >> executeWithRequest: request
110
+
111
+ | prefix names |
112
+ prefix := request stringArgumentNamed: 'prefix' default: ''.
113
+ names := (MyProject widgetNames select: [ :each | each beginsWith: prefix ]) asArray sorted.
114
+ ^ self
115
+ successResultText: names size asString , ' widget(s): ' , (', ' join: names) , '.'
116
+ data: { (#widgets -> names) } asDictionary
117
+ ```
118
+
119
+ `MyProject widgetNames` stands for your own code. What the rest is for:
120
+
121
+ - **`toolName`** is the key in one catalog shared with every other tool the
122
+ image has loaded, so pick a first word that is yours. `widget_list` is
123
+ safe; `class_search` is taken.
124
+ - **`description`** is read by a model deciding whether to call you. Say what
125
+ the tool answers and name the tools that come next, the way the example
126
+ points at `widget_get`.
127
+ - **`additionalKeywords`** are the words `tool_search` matches besides the
128
+ ones already in the name and title.
129
+ - **`annotations`** tell a client what the tool does to the image.
130
+ `MCPStructureToolAnnotations readOnly` promises it changes nothing;
131
+ `mayModify` says otherwise. A host may require a confirmation for the
132
+ second.
133
+ - **`buildInputSchema`** is the contract the server validates an incoming
134
+ call against before your code runs. `additionalProperties` defaults to
135
+ false, so a caller that misspells an argument is told so instead of having
136
+ it ignored. Every property takes a description; it is the only place the
137
+ caller learns what the argument means.
138
+
139
+ ## Being served
140
+
141
+ A tool is served because its package was loaded, and naming itself is how a
142
+ class says it is a tool. Nothing else is needed: load `MyProject-MCP` and
143
+ every server in the image has `widget_list`.
144
+
145
+ It is the tool's **own** name that serves it, not an inherited one. A
146
+ subclass of a tool is a variation of that tool, so specialising one does not
147
+ claim the name it already holds — give the subclass its own `toolName` when
148
+ it is a tool of its own.
149
+
150
+ A class that must stay out of every catalog says so itself:
151
+
152
+ ```smalltalk
153
+ MyProjectTestTool class >> shouldAutoRegister
154
+
155
+ ^ false
156
+ ```
157
+
158
+ That is the right thing for a test fixture, which would otherwise be served
159
+ by every server in the image whose tests are loaded.
160
+
161
+ ## Reading arguments
162
+
163
+ `executeWithRequest:` receives an `MCPToolRequest` whose arguments have
164
+ already been validated against your schema. Read them with the typed
165
+ accessors rather than by hand, so a missing or oddly typed value fails in one
166
+ place:
167
+
168
+ ```smalltalk
169
+ request stringArgumentNamed: 'name'.
170
+ request stringArgumentNamed: 'name' default: 'anything'.
171
+ request stringArgumentNamed: 'side' in: #( 'instance' 'class' ) default: 'instance'.
172
+ request booleanArgumentNamed: 'force' default: false.
173
+ request nonNegativeIntegerArgumentNamed: 'offset' default: 0.
174
+ request stringCollectionArgumentNamed: 'packages' default: #( ).
175
+ request hasArgumentNamed: 'prefix'.
176
+ ```
177
+
178
+ ## Answering
179
+
180
+ Two answers, both built by the tool:
181
+
182
+ ```smalltalk
183
+ self successResultText: 'Three widgets.' data: { (#widgets -> names) } asDictionary.
184
+ self errorResultText: 'No widget named ' , name , '.' details: { (#known -> names) } asDictionary
185
+ ```
186
+
187
+ The text is what a model reads; the data is the same answer in a shape a
188
+ program can use. Give both. A text that says only "Done." costs the caller
189
+ another round trip to find out what happened.
190
+
191
+ You do not have to catch to report a failure. An exception that escapes
192
+ `executeWithRequest:` is answered as an error result naming your tool, the
193
+ error class, the message and the frames of your own code; the server leaves
194
+ its own frames out. Build an error result yourself when you know something
195
+ the exception does not, such as what the caller should have asked for.
196
+
197
+ Which of the two reaches the client is the server's choice, not the tool's.
198
+ `toolResponseFields` defaults to `#content`, where the data is rendered into
199
+ the text. `useContentAndStructuredContentToolResponses` sends both, and
200
+ `useStructuredContentOnlyToolResponses` sends only the structured half. The
201
+ structured half carries your text as `summary` and your dictionary as `data`.
202
+
203
+ ### Answering a page
204
+
205
+ A tool that answers a list answers one page of it, so a long result never
206
+ arrives as one block a model has to read through. Three parts: the shared
207
+ `offset` argument, how big a page is, and the slicing. The listing tool
208
+ again, now that the list can be long:
209
+
210
+ ```smalltalk
211
+ MyProjectToolListWidgets >> buildInputSchema
212
+
213
+ ^ MCPStructureInputSchema new
214
+ type: 'object';
215
+ properties: { self offsetInputProperty };
216
+ required: #( );
217
+ yourself
218
+
219
+ MyProjectToolListWidgets >> defaultPageLimit
220
+
221
+ ^ 25
222
+
223
+ MyProjectToolListWidgets >> executeWithRequest: request
224
+
225
+ | page |
226
+ page := MCPPaginationResult
227
+ fromEntries: MyProject widgetNames asArray sorted
228
+ limit: self defaultPageLimit
229
+ offset: (request nonNegativeIntegerArgumentNamed: 'offset' default: 0).
230
+ ^ self
231
+ successResultText: page returnedCount asString , ' widget(s).'
232
+ data: (page asDictionaryWithEntriesKey: #widgets)
233
+ ```
234
+
235
+ `offsetInputProperty` is the same `offset` argument every paging tool takes,
236
+ declared once so they all describe it the same way. `MCPPaginationResult`
237
+ cuts the page and answers the shape MCP's own tools answer: your entries
238
+ under the key you name, a `pagination` summary beside them, and `nextOffset`
239
+ while entries remain — a caller asks again with that offset until it stops
240
+ coming.
241
+
242
+ Answering `defaultPageLimit` is what puts `pageSize` in your contract, so
243
+ `tool_get` can tell a caller how large a page is before asking for one. A
244
+ tool that answers a whole result rather than pages leaves it alone.
245
+
246
+ ## Groups and icons
247
+
248
+ `groupName` is how the catalog and the world-menu browser group related
249
+ tools. Pick one for your project and answer it on the shared superclass.
250
+
251
+ MCP reads an icon out of a tool name whose words it recognises — `class`,
252
+ `method`, `search`, `create` — which is how its own tools stay consistent
253
+ without anyone choosing. Your words are not in that vocabulary, so declare an
254
+ icon:
255
+
256
+ ```smalltalk
257
+ MyProjectTool class >> toolIconSpec
258
+
259
+ ^ MCPToolIconSpec baseIconNamed: #pharoHalfScale badgeIconNamed: #browse
260
+ ```
261
+
262
+ ## A resource
263
+
264
+ A resource is a document the host can put in front of the model without the
265
+ model asking. Where a tool answers a question, a resource is material:
266
+
267
+ ```smalltalk
268
+ MCPResource << #MyProjectWidgetResource
269
+ package: 'MyProject-MCP'
270
+ ```
271
+
272
+ ```smalltalk
273
+ MyProjectWidgetResource class >> resourceUri
274
+
275
+ ^ 'myproject://widgets'
276
+
277
+ MyProjectWidgetResource class >> resourceName
278
+
279
+ ^ 'widgets'
280
+
281
+ MyProjectWidgetResource >> title
282
+
283
+ ^ 'Widget Inventory'
284
+
285
+ MyProjectWidgetResource >> description
286
+
287
+ ^ 'Every widget this image holds, as JSON.'
288
+
289
+ MyProjectWidgetResource >> mimeType
290
+
291
+ ^ 'application/json'
292
+
293
+ MyProjectWidgetResource >> readContents
294
+
295
+ ^ self textContents: (STONJSON toStringPretty: { (#widgets -> MyProject widgetNames) } asDictionary)
296
+ ```
297
+
298
+ The URI is the key in the registry, and the same rule applies: a resource
299
+ that names itself is served.
300
+
301
+ ### A family of resources
302
+
303
+ When the document is one of many, named by something in the URI, subclass
304
+ `MCPResourceTemplate`. It names itself with a `resourceUriTemplate` in RFC
305
+ 6570 form, resolves one URI at a time through `resourceForUri:`, and may
306
+ suggest values for its variables:
307
+
308
+ ```smalltalk
309
+ MyProjectWidgetTemplate class >> resourceUriTemplate
310
+
311
+ ^ 'myproject://widget/{name}'
312
+
313
+ MyProjectWidgetTemplate >> resourceForUri: aUri
314
+
315
+ | name |
316
+ (aUri beginsWith: MyProjectOneWidgetResource resourceUri) ifFalse: [ ^ nil ].
317
+ name := aUri allButFirst: MyProjectOneWidgetResource resourceUri size.
318
+ ^ name
319
+ ifEmpty: [ nil ]
320
+ ifNotEmpty: [ MyProjectOneWidgetResource named: name ]
321
+
322
+ MyProjectWidgetTemplate >> completionsFor: aVariableName value: aPrefix context: aContextDictionary
323
+
324
+ aVariableName = 'name' ifFalse: [ ^ #( ) ].
325
+ ^ MyProject widgetNames select: [ :each | each beginsWith: aPrefix ]
326
+ ```
327
+
328
+ A template lists through `resources/templates/list` and never answers
329
+ contents of its own.
330
+
331
+ What it resolves to is an ordinary resource built per URI rather than
332
+ registered. It carries the name it was built with, answers its own `uri`, and
333
+ keeps itself out of the catalog, where one entry standing for the whole family
334
+ would be wrong:
335
+
336
+ ```smalltalk
337
+ MyProjectOneWidgetResource class >> resourceUri
338
+
339
+ ^ 'myproject://widget/'
340
+
341
+ MyProjectOneWidgetResource class >> shouldAutoRegister
342
+
343
+ ^ false
344
+
345
+ MyProjectOneWidgetResource class >> named: aName
346
+
347
+ ^ self new
348
+ widgetName: aName;
349
+ yourself
350
+
351
+ MyProjectOneWidgetResource >> uri
352
+
353
+ ^ self class resourceUri , self widgetName
354
+
355
+ MyProjectOneWidgetResource >> name
356
+
357
+ ^ self widgetName
358
+
359
+ MyProjectOneWidgetResource >> readContents
360
+
361
+ ^ self textContents: (MyProject describeWidgetNamed: self widgetName)
362
+ ```
363
+
364
+ ## A prompt
365
+
366
+ A prompt is user-controlled where a tool is model-controlled: a client
367
+ usually shows them as slash commands, so a prompt is something a person
368
+ reaches for deliberately.
369
+
370
+ ```smalltalk
371
+ MyProjectReviewPrompt class >> promptName
372
+
373
+ ^ 'review_widget'
374
+
375
+ MyProjectReviewPrompt >> title
376
+
377
+ ^ 'Review A Widget'
378
+
379
+ MyProjectReviewPrompt >> description
380
+
381
+ ^ 'Asks for a review of one widget.'
382
+
383
+ MyProjectReviewPrompt >> arguments
384
+
385
+ ^ { (MCPStructurePromptArgument name: 'widget' description: 'The widget to review.' required: true) }
386
+
387
+ MyProjectReviewPrompt >> messagesFor: anArgumentDictionary
388
+
389
+ ^ self userMessage: 'Review the widget named ' , (anArgumentDictionary at: 'widget')
390
+ , ' and say what you would change.'
391
+ ```
392
+
393
+ Like a template, a prompt may answer `completionsFor:value:context:` for its
394
+ own arguments.
395
+
396
+ ## Shaping the server's surface
397
+
398
+ Two words govern it. A **registered** tool is in the catalog: callable by
399
+ name, and findable with `tool_search` and `tool_get`. An **advertised** tool
400
+ is also in `tools/list`, which is the list a client shows a model up front.
401
+ Advertising decides what is shown, not what may be called.
402
+
403
+ A tool from outside MCP is registered and discoverable, not advertised, so it
404
+ costs nothing in the client's context until something looks for it. Answer
405
+ `'static'` from `defaultExposure` to be in `tools/list` from the start.
406
+
407
+ Against that default, a server is configured directly:
408
+
409
+ ```smalltalk
410
+ mcp := MCP new.
411
+ mcp addTool: MyProjectToolListWidgets. "or addTools: { A. B }"
412
+ mcp removeTool: 'image_evaluate'.
413
+ mcp advertiseTool: MyProjectToolListWidgets. "adds one to what is advertised"
414
+ mcp advertiseTools: { MyProjectToolListWidgets }. "replaces the advertised set"
415
+ mcp advertiseAllTools.
416
+ mcp advertiseToolDefaults.
417
+ mcp port: 4000.
418
+ mcp start.
419
+ ```
420
+
421
+ `MCP startOnPort:` on the class side answers the server already on that port,
422
+ or builds one with the default catalog. A server you configured is started
423
+ through the instance, as above.
424
+
425
+ For a server holding only your own tools:
426
+
427
+ ```smalltalk
428
+ mcp := MCP onlyTools: { MyProjectToolListWidgets. MyProjectToolGetWidget }.
429
+ mcp port: 4000.
430
+ mcp start.
431
+ ```
432
+
433
+ `onlyTools:` replaces the catalog with exactly those, advertises all of them,
434
+ and discovers nothing afterwards. That includes the catalog tools: a server
435
+ built this way has no `tool_search`, `tool_get` or `tool_call` unless you add
436
+ them back. Add `MCPToolSearchTools`, `MCPToolGetTool` and `MCPToolCallTool`
437
+ when you want a surface a model can explore, or build up from `MCP new` with
438
+ `addTool:` instead.
439
+
440
+ ## Testing what you serve
441
+
442
+ A server answers its JSON-RPC methods as plain messages, so a test needs no
443
+ socket and no port:
444
+
445
+ ```smalltalk
446
+ MyProjectToolTest >> testListingAnswersEveryWidget
447
+
448
+ | result |
449
+ result := (MCP new useContentAndStructuredContentToolResponses; yourself)
450
+ rpcToolCall: 'widget_list' withParams: Dictionary new.
451
+ self assert: (((result at: #structuredContent) at: #data) at: #widgets) equals: #( 'alpha' 'beta' 'gamma' ).
452
+ self assert: (((result at: #content) first at: #text) beginsWith: '3 widget(s)')
453
+ ```
454
+
455
+ Assert on the data rather than on the whole text: under the default
456
+ `#content`, the text a client receives is your summary with the data rendered
457
+ underneath it, so an equality test on it breaks whenever either half changes.
458
+
459
+ `rpcResourceRead:`, `rpcPromptGet:arguments:`, `rpcToolsList`,
460
+ `rpcResourcesList` and `rpcPromptsList` are there for the same purpose.
461
+
462
+ ## Things that bite
463
+
464
+ - **A name already taken.** The catalog holds one tool per name. Prefix with
465
+ a word that is yours.
466
+ - **A subclass of an existing tool is not served** until it names itself.
467
+ That is deliberate: it would otherwise claim its superclass's name and the
468
+ two would race for the key.
469
+ - **A fixture in a loaded test package is served** like anything else.
470
+ Answer `false` from `shouldAutoRegister`.
471
+ - **`onlyTools:` drops the catalog tools.** See above.
472
+ - **Nothing is saved.** Tools change the running image and never write it to
473
+ disk. `Smalltalk saveSession` is the caller's business, not the tool's.
@@ -275,7 +275,8 @@ makes one only when the image has none there. Starting a second server on a
275
275
  taken port fails after a couple of seconds, and the error now says when the
276
276
  port is held by another MCP in the same image.
277
277
 
278
- An inspected `MCP` object has two Spec tabs. The dashboard runs the server:
278
+ An inspected `MCP` object has a dashboard tab and an observability tab. The
279
+ dashboard runs the server:
279
280
 
280
281
  - server status, endpoint and port
281
282
  - debug mode
@@ -43,10 +43,10 @@ mcp useContentAndStructuredContentToolResponses.
43
43
  ## Compact Paging
44
44
 
45
45
  List and search tools are optimized for low token usage. A page is the tool's
46
- own size, chosen for what one of its entries costs to read: fifty classes,
47
- twenty-five methods or packages, twenty critique hits, ten repositories. When
48
- another page exists, `data` includes `nextOffset`; call the same tool again
49
- with that `offset` to continue.
46
+ own size, chosen for what one of its entries costs to read, so a page of
47
+ classes is longer than a page of repositories. The number is not something to
48
+ know in advance: when another page exists, `data` includes `nextOffset`; call
49
+ the same tool again with that `offset` to continue.
50
50
 
51
51
  A search answers what identifies each hit, not everything known about it.
52
52
  `repository_search` says how many packages a repository holds rather than
@@ -55,7 +55,7 @@ rather than restating its documentation on every hit.
55
55
 
56
56
  ## Naming Several Targets
57
57
 
58
- Four tools act on several targets in one call. The argument that names the
58
+ Some tools act on several targets in one call. The argument that names the
59
59
  target holds either one value or a list of them, and every other argument
60
60
  applies to all of them:
61
61
 
@@ -124,7 +124,7 @@ attempted.
124
124
 
125
125
  ## The Advertised Surface
126
126
 
127
- Fifteen tools are advertised directly: `tool_search`, `tool_get`, `tool_call`, `class_search`, `class_get`, `class_create`, `class_remove`, `method_search`, `method_source_search`, `method_reference_search`, `method_get`, `method_compile`, `method_remove`, `test_run` and `image_evaluate`. Every other tool is found with `tool_search` and called by name like any other; `tool_call` does the same for a client that will only send a name its own list holds.
127
+ What a client is shown up front is the advertised surface: the catalog tools, the everyday class and method operations, `test_run` and `image_evaluate`. Which ones exactly is what `tools/list` answers, and the client already holds that list, so read it there rather than from here. Every other tool is found with `tool_search` and called by name like any other; `tool_call` does the same for a client that will only send a name its own list holds.
128
128
 
129
129
  A server chooses its own surface before `start`:
130
130
 
@@ -37,7 +37,7 @@ http://127.0.0.1:4000
37
37
 
38
38
  If you changed the port in Pharo, update the MCP client configuration.
39
39
 
40
- MCP currently handles MCP calls over HTTP POST. Unsupported HTTP methods
40
+ MCP handles MCP calls over HTTP POST. Unsupported HTTP methods
41
41
  return method-not-allowed instead of hanging.
42
42
 
43
43
  ## `tools/list` Is Empty Or Stale
@@ -77,96 +77,10 @@ Run `npx @evref-bl/pharo-mcp install --host <host>` in the target project to
77
77
  install context-routed skills for project loading, version compatibility,
78
78
  image-to-Git handoff, CI reproduction, and standalone MCP recovery.
79
79
 
80
- ## Custom Tools
80
+ ## Serving Your Own
81
81
 
82
- Define custom tools as subclasses of `MCPTool` in your own package. At minimum,
83
- provide the class-side tool name and the instance-side metadata, schema, and
84
- execution protocol expected by `MCPTool`:
85
-
86
- ```smalltalk
87
- MCPTool << #MyProjectMCPTool
88
- slots: {};
89
- package: 'MyProject-MCP'
90
- ```
91
-
92
- ```smalltalk
93
- MyProjectMCPTool class >> toolName
94
-
95
- ^ 'my_project_tool'
96
-
97
- MyProjectMCPTool >> description
98
-
99
- ^ 'Do one project-specific operation.'
100
-
101
- MyProjectMCPTool >> buildInputSchema
102
-
103
- ^ MCPStructureInputSchema new
104
- type: 'object';
105
- properties: #( );
106
- required: #( );
107
- additionalProperties: false;
108
- yourself
109
-
110
- MyProjectMCPTool >> executeWithRequest: request
111
-
112
- ^ self
113
- successResultText: 'Operation completed.'
114
- data: Dictionary new
115
- ```
116
-
117
- By default, tools outside the MCP package are not auto-registered. If your
118
- package intentionally publishes a tool whenever it is loaded, opt in:
119
-
120
- ```smalltalk
121
- MyProjectMCPTool class >> shouldAutoRegister
122
-
123
- ^ true
124
- ```
125
-
126
- Auto-registered external tools are discoverable by default. Agents can find
127
- them with `tool_search`, inspect them with `tool_get`, and call them by name.
128
-
129
- `MCPToolRegistry` is the server's source of truth for registered tools. In
130
- normal use, configure it through the `MCP` instance messages below. The
131
- `tools/list` response contains only the static tool surface cached by the MCP
132
- instance; discoverable tools remain callable, and are found with `tool_search`
133
- and `tool_get`.
134
-
135
- Two words govern the surface. A **registered** tool is in the catalog, found
136
- with `tool_search` and `tool_get`, and callable. An **advertised** tool is also
137
- in `tools/list`. Advertising decides what a client is shown, not what it may
138
- call.
139
-
140
- For a server with only your own tools:
141
-
142
- ```smalltalk
143
- mcp := MCP onlyTools: { MyProjectMCPTool }.
144
- mcp start.
145
- ```
146
-
147
- `onlyTools:` replaces the catalog with exactly those tools and advertises all of
148
- them. Advertise a subset when some registered tools should stay discoverable
149
- through the catalog tools instead:
150
-
151
- ```smalltalk
152
- mcp := MCP onlyTools: { MyProjectMCPTool. MyOtherMCPTool }.
153
- mcp advertiseTools: { MyProjectMCPTool }.
154
- ```
155
-
156
- To add to the tools a server already has, rather than replacing them:
157
-
158
- ```smalltalk
159
- mcp := MCP new.
160
- mcp addTool: MyProjectMCPTool.
161
- mcp start.
162
- ```
163
-
164
- `addTool:` takes a class or a ready instance, and `addTools:` takes several.
165
- `removeTool:` takes one back out by name. `advertiseTools:` accepts classes,
166
- instances or names, and registers anything not in the catalog yet, so it can be
167
- called before or after adding. `advertiseAllTools` advertises everything, and
168
- `advertiseToolDefaults` returns to each tool's own default exposure.
169
-
170
- Use `defaultExposure` to decide whether a registered tool is directly
171
- advertised in `tools/list` (`'static'`) or kept discoverable through the catalog
172
- tools (`'discoverable'`, the default).
82
+ An image can serve tools, resources and prompts a project defines itself,
83
+ from that project's own package and without changing MCP. Read
84
+ [Building on MCP](building-on-mcp.md) for the server-only load, what a tool,
85
+ a resource and a prompt each need, and how the surface a client sees is
86
+ shaped.
@@ -31,6 +31,8 @@ guessing from source.
31
31
  `skills/pharo-mcp-recovery/SKILL.md`.
32
32
  - Before debugger sessions or repairs: read the local
33
33
  [debugging guide](pharo-references/debugging.md).
34
+ - Before giving a project MCP tools, resources or prompts of its own: read
35
+ [building on MCP](pharo-references/building-on-mcp.md).
34
36
 
35
37
  ## Workflow
36
38