@empyria/mcp 0.1.3 → 0.1.5

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.
@@ -0,0 +1,11 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "" # See documentation for possible values
9
+ directory: "/" # Location of package manifests
10
+ schedule:
11
+ interval: "daily"
package/AGENTS.md CHANGED
@@ -53,6 +53,23 @@ don't special-case the "no params" path back to an omitted `inputSchema`, it sil
53
53
  of whether it takes params). See the regression test in `test/Server.test.js` ("still receives a
54
54
  real extra context, not undefined").
55
55
 
56
+ ## Optional params (`defineSchema(params, { optional: true })`)
57
+
58
+ `@empyria/common`'s `defineSchema` (>= 0.1.4) accepts an `{ optional: true }` option: a property
59
+ is excluded from JSON Schema `required` when its own schema carries `optional: true` (that
60
+ marker is stripped before the property is embedded — not a real JSON Schema keyword, never
61
+ leaked into the tool's advertised inputSchema). Without the option (the default, and every call
62
+ elsewhere in the Empyria ecosystem — restate, moleculer), behavior is unchanged from before this
63
+ existed: every declared property is required, even one with its own `default`.
64
+ `registerService` in `lib/Server.js` always passes `{ optional: true }`, so any action's `params`
65
+ here can mark a field optional this way. Added upstream (not as a local derivative in this repo —
66
+ see git history for that discarded first attempt and why) 2026-08-26 for a real downstream need:
67
+ a service wrapping an S3-shaped API had several genuinely optional/defaulted params
68
+ (`contentType`, `expiresInSeconds`, ...) and `registerService`'s previous unconditional
69
+ `defineSchema` call would have silently advertised all of them as required — a real API-contract
70
+ regression, not a style preference. Usage: `params: { limit: { ...number(10), optional: true } }`.
71
+ Requires `@empyria/common` >= 0.1.4 — this package's own `package.json` is pinned there or later.
72
+
56
73
  ## Auth (`lib/Guard.js`)
57
74
 
58
75
  `extra.http.req` (the raw `Request`, with real header access via `.headers.get(name)`) is
package/lib/Server.js CHANGED
@@ -10,7 +10,9 @@ import { withMetaGuard } from './Guard.js'
10
10
  * @property {string} [description] - Shown to MCP clients as the tool's description.
11
11
  * @property {Object<string, Object>} [params] - Map of param name to JSON schema, built with
12
12
  * `@empyria/common`'s schema helpers (`string()`, `number()`, `bool()`, ...). Omit for a
13
- * tool that takes no arguments.
13
+ * tool that takes no arguments. A property's own schema may additionally carry
14
+ * `optional: true` to exclude it from the tool's `required` list — `registerService` passes
15
+ * `defineSchema`'s own `{ optional: true }` option (`@empyria/common` >= 0.1.4) to honor it.
14
16
  * @property {(params: Object, extra: Object) => *} handler - Receives the validated params
15
17
  * (or `{}` if no `params` schema was declared) and the MCP request's `extra` context.
16
18
  * May return anything JSON-serializable, a string, or an already MCP-shaped
@@ -64,7 +66,10 @@ function registerService(server, service) {
64
66
  // no-params action drops to a single-argument (extra) call instead — silently
65
67
  // handing the caller's own context object to `args` and leaving `extra` (and
66
68
  // therefore any auth context) undefined.
67
- config.inputSchema = fromJsonSchema(defineSchema(params ?? {}), empyriaJsonSchemaValidator)
69
+ config.inputSchema = fromJsonSchema(
70
+ defineSchema(params ?? {}, { optional: true }),
71
+ empyriaJsonSchemaValidator,
72
+ )
68
73
 
69
74
  server.registerTool(`${service.name}.${actionName}`, config, async (args, extra) => {
70
75
  const result = await handler(args ?? {}, extra)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empyria/mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "MCP (Model Context Protocol) helpers for the Empyria nanoservice framework",
5
5
  "license": "MIT",
6
6
  "author": "Imre Fazekas <imre.fazekas@icloud.com>",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@empyria/classification": "0.1.3",
28
- "@empyria/common": "0.1.3",
28
+ "@empyria/common": "0.1.5",
29
29
  "@modelcontextprotocol/client": "2.0.0",
30
30
  "@modelcontextprotocol/server": "2.0.0"
31
31
  },
@@ -169,4 +169,81 @@ describe('createMcpServer', () => {
169
169
  }),
170
170
  ).toThrow(/handler/)
171
171
  })
172
+
173
+ test('a param marked optional: true is excluded from required', async () => {
174
+ const Service = {
175
+ name: 'Svc',
176
+ actions: {
177
+ search: {
178
+ params: { query: string(), limit: { ...number(10), optional: true } },
179
+ handler: async (params) => params,
180
+ },
181
+ },
182
+ }
183
+ const { client } = await connected([Service])
184
+ const { tools } = await client.listTools()
185
+ expect(tools[0].inputSchema.required).toEqual(['query'])
186
+ })
187
+
188
+ test('the optional marker itself is stripped, never leaked into the advertised schema', async () => {
189
+ const Service = {
190
+ name: 'Svc',
191
+ actions: {
192
+ search: {
193
+ params: { limit: { ...number(10), optional: true } },
194
+ handler: async (params) => params,
195
+ },
196
+ },
197
+ }
198
+ const { client } = await connected([Service])
199
+ const { tools } = await client.listTools()
200
+ expect(tools[0].inputSchema.properties.limit).toEqual({ type: 'number', default: 10 })
201
+ })
202
+
203
+ test('a call omitting an optional param still succeeds, and its default is applied by the validator', async () => {
204
+ const Service = {
205
+ name: 'Svc',
206
+ actions: {
207
+ search: {
208
+ params: { query: string(), limit: { ...number(10), optional: true } },
209
+ handler: async (params) => params,
210
+ },
211
+ },
212
+ }
213
+ const { client } = await connected([Service])
214
+ const result = await client.callTool({ name: 'Svc.search', arguments: { query: 'x' } })
215
+ expect(result.isError).toBeFalsy()
216
+ expect(JSON.parse(result.content[0].text)).toEqual({ query: 'x', limit: 10 })
217
+ })
218
+
219
+ test('a call omitting an optional param with no default leaves it absent', async () => {
220
+ const Service = {
221
+ name: 'Svc',
222
+ actions: {
223
+ search: {
224
+ params: { query: string(), tag: { ...string(), optional: true } },
225
+ handler: async (params) => params,
226
+ },
227
+ },
228
+ }
229
+ const { client } = await connected([Service])
230
+ const result = await client.callTool({ name: 'Svc.search', arguments: { query: 'x' } })
231
+ expect(result.isError).toBeFalsy()
232
+ expect(JSON.parse(result.content[0].text)).toEqual({ query: 'x' })
233
+ })
234
+
235
+ test("a param with only a default (no explicit optional) is still required — unchanged from @empyria/common's own defineSchema", async () => {
236
+ const Service = {
237
+ name: 'Svc',
238
+ actions: {
239
+ search: {
240
+ params: { limit: number(10) },
241
+ handler: async (params) => params,
242
+ },
243
+ },
244
+ }
245
+ const { client } = await connected([Service])
246
+ const { tools } = await client.listTools()
247
+ expect(tools[0].inputSchema.required).toEqual(['limit'])
248
+ })
172
249
  })