@coffeerunhobby/mcp-ha-connect 1.7.4 → 1.7.6

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.
Files changed (65) hide show
  1. package/.codex/review-rules.md +16 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/AGENTS.md +8 -15
  4. package/README.md +9 -1
  5. package/dist/permissions/index.d.ts +8 -0
  6. package/dist/permissions/index.js +15 -0
  7. package/dist/permissions/index.js.map +1 -1
  8. package/dist/server/auth.js +10 -1
  9. package/dist/server/auth.js.map +1 -1
  10. package/dist/server/common.d.ts +15 -1
  11. package/dist/server/common.js +43 -9
  12. package/dist/server/common.js.map +1 -1
  13. package/dist/server/stream.d.ts +1 -1
  14. package/dist/server/stream.js +6 -2
  15. package/dist/server/stream.js.map +1 -1
  16. package/dist/tools/common.d.ts +10 -0
  17. package/dist/tools/common.js +37 -0
  18. package/dist/tools/common.js.map +1 -1
  19. package/dist/tools/homeassistant/controlClimate.js +4 -0
  20. package/dist/tools/homeassistant/controlClimate.js.map +1 -1
  21. package/dist/tools/homeassistant/controlCover.js +4 -0
  22. package/dist/tools/homeassistant/controlCover.js.map +1 -1
  23. package/dist/tools/homeassistant/controlFan.js +4 -0
  24. package/dist/tools/homeassistant/controlFan.js.map +1 -1
  25. package/dist/tools/homeassistant/controlLight.js +4 -0
  26. package/dist/tools/homeassistant/controlLight.js.map +1 -1
  27. package/dist/tools/homeassistant/controlMediaPlayer.js +4 -0
  28. package/dist/tools/homeassistant/controlMediaPlayer.js.map +1 -1
  29. package/dist/tools/homeassistant/entityAction.js +4 -0
  30. package/dist/tools/homeassistant/entityAction.js.map +1 -1
  31. package/dist/tools/homeassistant/entityGuard.d.ts +15 -0
  32. package/dist/tools/homeassistant/entityGuard.js +25 -0
  33. package/dist/tools/homeassistant/entityGuard.js.map +1 -0
  34. package/dist/tools/homeassistant/sceneAndScript.js +7 -0
  35. package/dist/tools/homeassistant/sceneAndScript.js.map +1 -1
  36. package/dist/tools/omada/graph.d.ts +6 -0
  37. package/dist/tools/omada/graph.js +11 -2
  38. package/dist/tools/omada/graph.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/docs/CHANGELOG.md +14 -0
  42. package/openapi.json +1 -1
  43. package/package.json +1 -1
  44. package/scripts/generate-jwt.ts +8 -9
  45. package/src/permissions/index.ts +18 -0
  46. package/src/server/auth.ts +11 -1
  47. package/src/server/common.ts +53 -11
  48. package/src/server/stream.ts +7 -2
  49. package/src/tools/common.ts +42 -0
  50. package/src/tools/homeassistant/controlClimate.ts +3 -0
  51. package/src/tools/homeassistant/controlCover.ts +3 -0
  52. package/src/tools/homeassistant/controlFan.ts +3 -0
  53. package/src/tools/homeassistant/controlLight.ts +3 -0
  54. package/src/tools/homeassistant/controlMediaPlayer.ts +3 -0
  55. package/src/tools/homeassistant/entityAction.ts +3 -0
  56. package/src/tools/homeassistant/entityGuard.ts +28 -0
  57. package/src/tools/homeassistant/sceneAndScript.ts +5 -0
  58. package/src/tools/omada/graph.ts +12 -2
  59. package/src/version.ts +1 -1
  60. package/tests/permissions/index.test.ts +23 -0
  61. package/tests/scripts/generateJwt.test.ts +44 -0
  62. package/tests/server/auth.test.ts +25 -0
  63. package/tests/server/toolVisibility.http.test.ts +111 -0
  64. package/tests/server/toolVisibility.test.ts +130 -0
  65. package/tests/tools/homeassistant/handlers.test.ts +41 -0
@@ -1 +1 @@
1
- {"version":3,"file":"controlFan.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/controlFan.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK3F,MAAM,UAAU,sBAAsB,CAAC,MAAiB,EAAE,MAAgB;IACxE,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,WAAW,EAAE,gEAAgE;QAC7E,WAAW,EAAE,gBAAgB,CAAC,KAAK;KACpC,EACD,eAAe,CAAC,YAAY,EAAE,KAAK,EAAE,IAAoB,EAAE,EAAE;QAC3D,IAAI,OAAO,GAAW,IAAI,CAAC,MAAM,CAAC;QAClC,MAAM,WAAW,GAA4B,EAAE,CAAC;QAEhD,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAChC,OAAO,GAAG,gBAAgB,CAAC;YAC3B,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;gBAAE,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAC9E,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YACvC,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;gBAAE,WAAW,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACjF,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;YAC3C,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;gBAAE,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3E,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,KAAK;YACb,OAAO;YACP,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;YACrC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;SAC5E,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAClH,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"controlFan.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/controlFan.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAKxD,MAAM,UAAU,sBAAsB,CAAC,MAAiB,EAAE,MAAgB;IACxE,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,WAAW,EAAE,gEAAgE;QAC7E,WAAW,EAAE,gBAAgB,CAAC,KAAK;KACpC,EACD,eAAe,CAAC,YAAY,EAAE,KAAK,EAAE,IAAoB,EAAE,EAAE;QAC3D,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,IAAI,OAAO,GAAW,IAAI,CAAC,MAAM,CAAC;QAClC,MAAM,WAAW,GAA4B,EAAE,CAAC;QAEhD,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAChC,OAAO,GAAG,gBAAgB,CAAC;YAC3B,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;gBAAE,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAC9E,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YACvC,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;gBAAE,WAAW,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACjF,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;YAC3C,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;gBAAE,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3E,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,KAAK;YACb,OAAO;YACP,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;YACrC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;SAC5E,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAClH,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
@@ -2,11 +2,15 @@
2
2
  * controlLight tool - Control lights with advanced options
3
3
  */
4
4
  import { controlLightSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
5
+ import { entityNotFoundResult } from './entityGuard.js';
5
6
  export function registerControlLightTool(server, client) {
6
7
  server.registerTool('controlLight', {
7
8
  description: 'Control a light with advanced options like brightness, color, and color temperature.',
8
9
  inputSchema: controlLightSchema.shape,
9
10
  }, wrapToolHandler('controlLight', async (args) => {
11
+ const missing = await entityNotFoundResult(client, args.entity_id);
12
+ if (missing)
13
+ return missing;
10
14
  const serviceData = {};
11
15
  if (args.brightness_pct !== undefined)
12
16
  serviceData.brightness_pct = args.brightness_pct;
@@ -1 +1 @@
1
- {"version":3,"file":"controlLight.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/controlLight.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK7F,MAAM,UAAU,wBAAwB,CAAC,MAAiB,EAAE,MAAgB;IAC1E,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EAAE,sFAAsF;QACnG,WAAW,EAAE,kBAAkB,CAAC,KAAK;KACtC,EACD,eAAe,CAAC,cAAc,EAAE,KAAK,EAAE,IAAsB,EAAE,EAAE;QAC/D,MAAM,WAAW,GAA4B,EAAE,CAAC;QAChD,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS;YAAE,WAAW,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;QACxF,IAAI,IAAI,CAAC,iBAAiB,KAAK,SAAS;YAAE,WAAW,CAAC,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,CAAC;QACjG,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;YAAE,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QACzE,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;YAAE,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAE5E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,IAAI,CAAC,MAAM;YACpB,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;YACrC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;SAC5E,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IACzI,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"controlLight.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/controlLight.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAKxD,MAAM,UAAU,wBAAwB,CAAC,MAAiB,EAAE,MAAgB;IAC1E,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EAAE,sFAAsF;QACnG,WAAW,EAAE,kBAAkB,CAAC,KAAK;KACtC,EACD,eAAe,CAAC,cAAc,EAAE,KAAK,EAAE,IAAsB,EAAE,EAAE;QAC/D,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,MAAM,WAAW,GAA4B,EAAE,CAAC;QAChD,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS;YAAE,WAAW,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;QACxF,IAAI,IAAI,CAAC,iBAAiB,KAAK,SAAS;YAAE,WAAW,CAAC,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,CAAC;QACjG,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;YAAE,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QACzE,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;YAAE,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAE5E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,IAAI,CAAC,MAAM;YACpB,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;YACrC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;SAC5E,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IACzI,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
@@ -2,11 +2,15 @@
2
2
  * controlMediaPlayer tool - Control media players
3
3
  */
4
4
  import { controlMediaPlayerSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
5
+ import { entityNotFoundResult } from './entityGuard.js';
5
6
  export function registerControlMediaPlayerTool(server, client) {
6
7
  server.registerTool('controlMediaPlayer', {
7
8
  description: 'Control a media player with playback, volume, and media selection.',
8
9
  inputSchema: controlMediaPlayerSchema.shape,
9
10
  }, wrapToolHandler('controlMediaPlayer', async (args) => {
11
+ const missing = await entityNotFoundResult(client, args.entity_id);
12
+ if (missing)
13
+ return missing;
10
14
  const actionMap = {
11
15
  play: 'media_play',
12
16
  pause: 'media_pause',
@@ -1 +1 @@
1
- {"version":3,"file":"controlMediaPlayer.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/controlMediaPlayer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,wBAAwB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAKnG,MAAM,UAAU,8BAA8B,CAAC,MAAiB,EAAE,MAAgB;IAChF,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,WAAW,EAAE,oEAAoE;QACjF,WAAW,EAAE,wBAAwB,CAAC,KAAK;KAC5C,EACD,eAAe,CAAC,oBAAoB,EAAE,KAAK,EAAE,IAA4B,EAAE,EAAE;QAC3E,MAAM,SAAS,GAA2B;YACxC,IAAI,EAAE,YAAY;YAClB,KAAK,EAAE,aAAa;YACpB,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,kBAAkB;YACxB,QAAQ,EAAE,sBAAsB;YAChC,UAAU,EAAE,YAAY;YACxB,WAAW,EAAE,aAAa;SAC3B,CAAC;QAEF,MAAM,WAAW,GAA4B,EAAE,CAAC;QAChD,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACpE,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QAC/C,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa,IAAI,IAAI,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YACxE,WAAW,CAAC,eAAe,GAAG,IAAI,CAAC,eAAe,CAAC;QACrD,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,cAAc;YACtB,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM;YAC9C,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;YACrC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;SAC5E,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAClH,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"controlMediaPlayer.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/controlMediaPlayer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,wBAAwB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACnG,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAKxD,MAAM,UAAU,8BAA8B,CAAC,MAAiB,EAAE,MAAgB;IAChF,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,WAAW,EAAE,oEAAoE;QACjF,WAAW,EAAE,wBAAwB,CAAC,KAAK;KAC5C,EACD,eAAe,CAAC,oBAAoB,EAAE,KAAK,EAAE,IAA4B,EAAE,EAAE;QAC3E,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,MAAM,SAAS,GAA2B;YACxC,IAAI,EAAE,YAAY;YAClB,KAAK,EAAE,aAAa;YACpB,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,kBAAkB;YACxB,QAAQ,EAAE,sBAAsB;YAChC,UAAU,EAAE,YAAY;YACxB,WAAW,EAAE,aAAa;SAC3B,CAAC;QAEF,MAAM,WAAW,GAA4B,EAAE,CAAC;QAChD,IAAI,IAAI,CAAC,MAAM,KAAK,YAAY,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACpE,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QAC/C,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,KAAK,aAAa,IAAI,IAAI,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YACxE,WAAW,CAAC,eAAe,GAAG,IAAI,CAAC,eAAe,CAAC;QACrD,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,cAAc;YACtB,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM;YAC9C,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE;YACrC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;SAC5E,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAClH,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
@@ -2,6 +2,7 @@
2
2
  * entityAction tool - Simple entity actions (turn_on, turn_off, toggle)
3
3
  */
4
4
  import { entityActionSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
5
+ import { entityNotFoundResult } from './entityGuard.js';
5
6
  export function registerEntityActionTool(server, client) {
6
7
  server.registerTool('entityAction', {
7
8
  description: 'Perform a simple action on an entity (turn_on, turn_off, toggle). Automatically detects the domain.',
@@ -11,6 +12,9 @@ export function registerEntityActionTool(server, client) {
11
12
  if (!domain) {
12
13
  return toToolResult({ error: 'Invalid entity_id format', hint: 'entity_id must be in domain.name format such as light.living_room — use the searchEntities tool to discover valid entity IDs' }, true);
13
14
  }
15
+ const missing = await entityNotFoundResult(client, entity_id);
16
+ if (missing)
17
+ return missing;
14
18
  const result = await client.callService({
15
19
  domain,
16
20
  service: action,
@@ -1 +1 @@
1
- {"version":3,"file":"entityAction.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/entityAction.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK7F,MAAM,UAAU,wBAAwB,CAAC,MAAiB,EAAE,MAAgB;IAC1E,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EAAE,qGAAqG;QAClH,WAAW,EAAE,kBAAkB,CAAC,KAAK;KACtC,EACD,eAAe,CAAC,cAAc,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAoB,EAAE,EAAE;QAChF,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,IAAI,EAAE,8HAA8H,EAAE,EAAE,IAAI,CAAC,CAAC;QACzM,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM;YACN,OAAO,EAAE,MAAM;YACf,MAAM,EAAE,EAAE,SAAS,EAAE;SACtB,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IACrF,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"entityAction.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/entityAction.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAKxD,MAAM,UAAU,wBAAwB,CAAC,MAAiB,EAAE,MAAgB;IAC1E,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;QACE,WAAW,EAAE,qGAAqG;QAClH,WAAW,EAAE,kBAAkB,CAAC,KAAK;KACtC,EACD,eAAe,CAAC,cAAc,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAoB,EAAE,EAAE;QAChF,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,IAAI,EAAE,8HAA8H,EAAE,EAAE,IAAI,CAAC,CAAC;QACzM,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC9D,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM;YACN,OAAO,EAAE,MAAM;YACf,MAAM,EAAE,EAAE,SAAS,EAAE;SACtB,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IACrF,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Existence guard for entity-targeted control tools.
3
+ *
4
+ * Home Assistant silently ignores service calls whose target entity_id does not
5
+ * exist: the call returns 200 with an empty change list. Without this guard the
6
+ * tool would report `success: true` for an action that never happened, so an
7
+ * LLM client would believe a typo'd light was switched on.
8
+ */
9
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
10
+ import type { HaClient } from '../../haClient/index.js';
11
+ /**
12
+ * Returns an error result when `entityId` does not exist in Home Assistant,
13
+ * or `null` when it does (the caller should then proceed with the action).
14
+ */
15
+ export declare function entityNotFoundResult(client: HaClient, entityId: string): Promise<CallToolResult | null>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Existence guard for entity-targeted control tools.
3
+ *
4
+ * Home Assistant silently ignores service calls whose target entity_id does not
5
+ * exist: the call returns 200 with an empty change list. Without this guard the
6
+ * tool would report `success: true` for an action that never happened, so an
7
+ * LLM client would believe a typo'd light was switched on.
8
+ */
9
+ import { toToolResult } from '../common.js';
10
+ /**
11
+ * Returns an error result when `entityId` does not exist in Home Assistant,
12
+ * or `null` when it does (the caller should then proceed with the action).
13
+ */
14
+ export async function entityNotFoundResult(client, entityId) {
15
+ const state = await client.getState(entityId);
16
+ if (state) {
17
+ return null;
18
+ }
19
+ return toToolResult({
20
+ error: 'Entity not found',
21
+ entity_id: entityId,
22
+ hint: 'No action was performed. Use the searchEntities tool with a keyword to find the correct entity_id',
23
+ }, true);
24
+ }
25
+ //# sourceMappingURL=entityGuard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityGuard.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/entityGuard.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAgB,EAAE,QAAgB;IAC3E,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,YAAY,CAAC;QAClB,KAAK,EAAE,kBAAkB;QACzB,SAAS,EAAE,QAAQ;QACnB,IAAI,EAAE,mGAAmG;KAC1G,EAAE,IAAI,CAAC,CAAC;AACX,CAAC"}
@@ -2,11 +2,15 @@
2
2
  * Scene and Script tools - Activate scenes and run scripts
3
3
  */
4
4
  import { entityIdSchema, runScriptSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
5
+ import { entityNotFoundResult } from './entityGuard.js';
5
6
  export function registerActivateSceneTool(server, client) {
6
7
  server.registerTool('activateScene', {
7
8
  description: 'Activate a Home Assistant scene.',
8
9
  inputSchema: entityIdSchema.shape,
9
10
  }, wrapToolHandler('activateScene', async ({ entity_id }) => {
11
+ const missing = await entityNotFoundResult(client, entity_id);
12
+ if (missing)
13
+ return missing;
10
14
  const result = await client.callService({
11
15
  domain: 'scene',
12
16
  service: 'turn_on',
@@ -20,6 +24,9 @@ export function registerRunScriptTool(server, client) {
20
24
  description: 'Run a Home Assistant script with optional variables.',
21
25
  inputSchema: runScriptSchema.shape,
22
26
  }, wrapToolHandler('runScript', async ({ entity_id, variables }) => {
27
+ const missing = await entityNotFoundResult(client, entity_id);
28
+ if (missing)
29
+ return missing;
23
30
  const result = await client.callService({
24
31
  domain: 'script',
25
32
  service: 'turn_on',
@@ -1 +1 @@
1
- {"version":3,"file":"sceneAndScript.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/sceneAndScript.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAM1G,MAAM,UAAU,yBAAyB,CAAC,MAAiB,EAAE,MAAgB;IAC3E,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,WAAW,EAAE,kCAAkC;QAC/C,WAAW,EAAE,cAAc,CAAC,KAAK;KAClC,EACD,eAAe,CAAC,eAAe,EAAE,KAAK,EAAE,EAAE,SAAS,EAAgB,EAAE,EAAE;QACrE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,SAAS;YAClB,MAAM,EAAE,EAAE,SAAS,EAAE;SACtB,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAClG,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAiB,EAAE,MAAgB;IACvE,MAAM,CAAC,YAAY,CACjB,WAAW,EACX;QACE,WAAW,EAAE,sDAAsD;QACnE,WAAW,EAAE,eAAe,CAAC,KAAK;KACnC,EACD,eAAe,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAiB,EAAE,EAAE;QAC7E,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,QAAQ;YAChB,OAAO,EAAE,SAAS;YAClB,MAAM,EAAE,EAAE,SAAS,EAAE;YACrB,YAAY,EAAE,SAAgD;SAC/D,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IACjG,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"sceneAndScript.js","sourceRoot":"","sources":["../../../src/tools/homeassistant/sceneAndScript.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1G,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAMxD,MAAM,UAAU,yBAAyB,CAAC,MAAiB,EAAE,MAAgB;IAC3E,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,WAAW,EAAE,kCAAkC;QAC/C,WAAW,EAAE,cAAc,CAAC,KAAK;KAClC,EACD,eAAe,CAAC,eAAe,EAAE,KAAK,EAAE,EAAE,SAAS,EAAgB,EAAE,EAAE;QACrE,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC9D,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,SAAS;YAClB,MAAM,EAAE,EAAE,SAAS,EAAE;SACtB,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAClG,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAiB,EAAE,MAAgB;IACvE,MAAM,CAAC,YAAY,CACjB,WAAW,EACX;QACE,WAAW,EAAE,sDAAsD;QACnE,WAAW,EAAE,eAAe,CAAC,KAAK;KACnC,EACD,eAAe,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAiB,EAAE,EAAE;QAC7E,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC9D,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;YACtC,MAAM,EAAE,QAAQ;YAChB,OAAO,EAAE,SAAS;YAClB,MAAM,EAAE,EAAE,SAAS,EAAE;YACrB,YAAY,EAAE,SAAgD;SAC/D,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IACjG,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CACvB,CAAC;AACJ,CAAC"}
@@ -55,6 +55,12 @@ export declare const readSchema: z.ZodObject<{
55
55
  export declare function createBrowseHandler(client: OmadaClient): (args: {
56
56
  path: string;
57
57
  }, extra: ToolExtra) => Promise<import("@modelcontextprotocol/sdk/types").CallToolResult>;
58
+ /**
59
+ * `omada_read` is usable iff the caller holds the permission of at least one readable
60
+ * (fetchable) resource node; otherwise it is hidden from `tools/list`. Kept next to the
61
+ * handler so the rule cannot drift from the per-path check it summarises.
62
+ */
63
+ export declare function canReadAnyResource(callerPermissions: number): boolean;
58
64
  /** Shared `omada_read` handler — same dual-face reuse as createBrowseHandler. */
59
65
  export declare function createReadHandler(client: OmadaClient): (args: {
60
66
  path: string;
@@ -16,8 +16,8 @@
16
16
  */
17
17
  import { z } from 'zod';
18
18
  import { hasPermission, getPermissionNames } from '../../permissions/index.js';
19
- import { toToolResult, wrapToolHandler, getCallerPermissions, Permission } from '../common.js';
20
- import { childrenOf, getResourceNode, normalizePath } from './namespace.js';
19
+ import { toToolResult, wrapToolHandler, getCallerPermissions, setToolVisibilityRule, Permission } from '../common.js';
20
+ import { OMADA_RESOURCES, childrenOf, getResourceNode, normalizePath } from './namespace.js';
21
21
  /** Compact, model-friendly metadata for a node (used in browse output). */
22
22
  function nodeSummary(node) {
23
23
  return {
@@ -76,8 +76,17 @@ export function createBrowseHandler(client) {
76
76
  });
77
77
  }, Permission.QUERY);
78
78
  }
79
+ /**
80
+ * `omada_read` is usable iff the caller holds the permission of at least one readable
81
+ * (fetchable) resource node; otherwise it is hidden from `tools/list`. Kept next to the
82
+ * handler so the rule cannot drift from the per-path check it summarises.
83
+ */
84
+ export function canReadAnyResource(callerPermissions) {
85
+ return OMADA_RESOURCES.some((node) => node.fetch !== undefined && hasPermission(callerPermissions, node.permission));
86
+ }
79
87
  /** Shared `omada_read` handler — same dual-face reuse as createBrowseHandler. */
80
88
  export function createReadHandler(client) {
89
+ setToolVisibilityRule('omada_read', canReadAnyResource);
81
90
  // No static permission: each path declares its own, enforced below (fail-closed).
82
91
  return wrapToolHandler('omada_read', async (args, extra) => {
83
92
  const norm = normalizePath(args.path);
@@ -1 +1 @@
1
- {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../../src/tools/omada/graph.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../../../src/tools/omada/graph.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Package version - hardcoded at build time for reliability
3
3
  * Keep in sync with package.json version
4
4
  */
5
- export declare const VERSION = "1.7.4";
5
+ export declare const VERSION = "1.7.6";
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  * Package version - hardcoded at build time for reliability
3
3
  * Keep in sync with package.json version
4
4
  */
5
- export const VERSION = '1.7.4';
5
+ export const VERSION = '1.7.6';
6
6
  //# sourceMappingURL=version.js.map
package/docs/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ### 1.7.6
2
+ - **`omada_read` is hidden from callers who can read no Omada resource.** It has no single static permission (it checks per resource path), so 1.7.5 still listed it to every caller, including unmapped tokens that could read nothing. A new per-tool visibility rule lists it only when the caller holds the permission of at least one readable node (e.g. a `QUERY` or an `ADMIN`-only mask); a `NONE`/`NOTIFY`-only caller no longer sees it. Per-path checks are unchanged.
3
+ - **CI: `actions/checkout` and `actions/setup-node` v4 → v7** (Node 24 runtime; v4 targeted the deprecated Node 20 action runtime). No workflow changes were needed: the breaking changes in v5–v7 concern automatic caching (not triggered — no `packageManager` field) and credential persistence (CI never pushes with git).
4
+ - `AGENTS.md`: the GHCR push token is fetched from the maintainer's password manager at push time — never stored as an environment variable.
5
+ - +4 tests (`omada_read` visibility for NONE / QUERY / ADMIN-only / NOTIFY-only masks).
6
+
7
+ ### 1.7.5
8
+ - **Security — `tools/list` is filtered by the caller's permissions.** A token is only offered the tools it may call (e.g. a `readonly` token no longer sees `restartHomeAssistant`, `deleteAutomation`, `invokeAction` or the Omada write tools). Previously every valid token was shown all tools; calls were already denied, but LLM clients planned around tools they could not use and the admin surface was disclosed. Each tool's static permission is recorded by `wrapToolHandler`; `createServer({ callerPermissions })` disables the rest, so the SDK omits them from the list and rejects calls to them. Per-call RBAC is unchanged; stdio (local trust) is unfiltered; `omada_read`/`omada_browse` stay listed (they gate per resource path).
9
+ - **Fix — control tools no longer report success for a non-existent entity.** Home Assistant silently ignores service calls to unknown `entity_id`s, so `entityAction`, `controlLight`, `controlClimate`, `controlFan`, `controlCover`, `controlMediaPlayer`, `activateScene` and `runScript` returned `success: true` for typos. They now check existence first and return `isError` "Entity not found" ("No action was performed") without calling the service.
10
+ - **Warning — permissive `defaultRole`.** With bearer auth, the server logs a startup warning when `MCP_PERMISSIONS_CONFIG` `defaultRole` grants anything, since it applies to every valid token with an unmapped `sub`. README now recommends `defaultRole: NONE` plus one `users` entry per client (and fixes the `ADMIN` role row, which wrongly said "all except AI").
11
+ - **Fix — `generate:jwt` prints the token once.** stdout carries only the bare token; details go to stderr, so `TOKEN=$(npm run -s generate:jwt -- ...)` captures exactly one token.
12
+ - Added `.codex/review-rules.md` (the repo's hard rules for pre-release code review).
13
+ - +26 tests: permission-filtered `tools/list` over an MCP client and over the real HTTP path (bearer auth → stream transport, incl. interleaved callers), the entity guard for all eight tools, `defaultRoleWarning` and its wiring in the auth middleware, and the `generate:jwt` stdout contract (subprocess).
14
+
1
15
  ### 1.7.4
2
16
  - **Security — cleared every advisory in the dependency tree** (`npm audit` and `npm audit --omit=dev` both report 0). Runtime range declarations are unchanged; all fixes are in-range lockfile bumps:
3
17
  - Production: `undici` 6.28.0→6.29.0 (WebSocket DoS ×2, retry-interceptor response splitting), `fast-uri` 3.1.6→3.1.8 (authority injection / host confusion; via ajv), `hono` 4.13.3→4.13.11, `ip-address` 10.5.0→10.7.2, `qs` 6.15.3→6.16.0 (all three transitive under the SDK's express stack).
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Home Assistant MCP Wrapper",
5
- "version": "1.7.4",
5
+ "version": "1.7.6",
6
6
  "description": "REST endpoints that wrap MCP tools for Home Assistant integration."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffeerunhobby/mcp-ha-connect",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "Smart home MCP server for Home Assistant plus Local AI integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -107,16 +107,15 @@ function main() {
107
107
 
108
108
  const token = createJwt(payload, secret);
109
109
 
110
- console.log('\n=== JWT Token Generated ===\n');
111
- console.log('Subject:', sub);
112
- console.log('Issued at:', new Date(iat * 1000).toISOString());
113
- console.log('Expires at:', new Date(expTime * 1000).toISOString());
114
- console.log('Duration:', exp);
115
- console.log('\n--- Token ---\n');
110
+ // Details go to stderr; stdout carries ONLY the bare token, exactly once, so
111
+ // `TOKEN=$(npm run -s generate:jwt -- ...)` captures one usable token.
112
+ console.error('\n=== JWT Token Generated ===\n');
113
+ console.error('Subject:', sub);
114
+ console.error('Issued at:', new Date(iat * 1000).toISOString());
115
+ console.error('Expires at:', new Date(expTime * 1000).toISOString());
116
+ console.error('Duration:', exp);
117
+ console.error('\nUsage: curl -H "Authorization: Bearer <token>" http://localhost:3000/mcp ...\n');
116
118
  console.log(token);
117
- console.log('\n--- Usage ---\n');
118
- console.log(`curl -H "Authorization: Bearer ${token}" http://localhost:3000/mcp ...`);
119
- console.log('');
120
119
  }
121
120
 
122
121
  main();
@@ -80,6 +80,24 @@ export function getUserPermissions(sub: string | undefined, config: PermissionsC
80
80
  return resolveRoleName(user.role);
81
81
  }
82
82
 
83
+ /**
84
+ * Warning text when `defaultRole` grants anything, or `undefined` when it is NONE.
85
+ *
86
+ * `defaultRole` applies to every validly signed token whose `sub` is not listed in
87
+ * `users` — so a non-NONE default silently gives unmapped clients real access. The
88
+ * recommended setup is `defaultRole: none` plus one explicit `users` entry per client.
89
+ */
90
+ export function defaultRoleWarning(config: PermissionsConfig): string | undefined {
91
+ const mask = resolveRoleName(config.defaultRole);
92
+ if (mask === Role.NONE) {
93
+ return undefined;
94
+ }
95
+ return (
96
+ `MCP_PERMISSIONS_CONFIG defaultRole is "${config.defaultRole}": any valid token with an unmapped sub ` +
97
+ `gets [${getPermissionNames(mask).join(', ')}]. Recommended: defaultRole "none" and one users[] entry per client.`
98
+ );
99
+ }
100
+
83
101
  /**
84
102
  * Check if a permission mask includes required permission
85
103
  */
@@ -5,7 +5,7 @@
5
5
  import type { IncomingMessage, ServerResponse } from 'node:http';
6
6
  import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
7
7
  import { verifyJwt, type JwtPayload } from '../utils/jwt.js';
8
- import { getUserPermissions, Role, type PermissionsConfig } from '../permissions/index.js';
8
+ import { defaultRoleWarning, getUserPermissions, Role, type PermissionsConfig } from '../permissions/index.js';
9
9
  import { logger } from '../utils/logger.js';
10
10
 
11
11
  export type AuthMethod = 'none' | 'bearer';
@@ -99,6 +99,16 @@ function validateJwt(
99
99
  export function createAuthMiddleware(config: AuthConfig): (req: IncomingMessage, res: ServerResponse) => boolean {
100
100
  const { method, secret, permissions: permConfig, skipPaths = [], requireExp, issuer, audience } = config;
101
101
 
102
+ // Loudly flag a permissive defaultRole: it silently grants every valid token whose
103
+ // sub is unmapped. Lives here (not in server startup) because this middleware is
104
+ // what applies defaultRole, so the warning cannot be bypassed.
105
+ if (method === 'bearer' && permConfig) {
106
+ const warning = defaultRoleWarning(permConfig);
107
+ if (warning) {
108
+ logger.warn(warning);
109
+ }
110
+ }
111
+
102
112
  return (req: IncomingMessage, res: ServerResponse): boolean => {
103
113
  if (method === 'none') {
104
114
  // No auth = full permissions via AuthInfo.extra
@@ -3,11 +3,12 @@
3
3
  * Shared across all transport types (stdio, SSE, stream)
4
4
  */
5
5
 
6
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { McpServer, type RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
7
7
  import type { HaClient } from '../haClient/index.js';
8
8
  import type { LocalAIClient } from '../localAI/index.js';
9
9
  import type { OmadaClient } from '../omadaClient/index.js';
10
10
  import { registerAllTools } from '../tools/index.js';
11
+ import { isToolVisibleTo } from '../tools/common.js';
11
12
  import type { OmadaRegistrationMode } from '../tools/omada/index.js';
12
13
  import type { RestAction } from '../tools/infra/index.js';
13
14
  import { registerAllResources } from '../resources/index.js';
@@ -23,10 +24,17 @@ export interface CreateServerOptions {
23
24
  restActions?: Record<string, RestAction>;
24
25
  /** Tool registration strategy for the Omada plugin (default 'eager'). */
25
26
  toolRegistrationMode?: OmadaRegistrationMode;
27
+ /**
28
+ * The caller's permission mask. When set, tools whose static permission the
29
+ * caller lacks are disabled, which hides them from `tools/list` (and the SDK
30
+ * rejects calls to them). Undefined = no filtering (stdio local trust).
31
+ * Execution-time RBAC in wrapToolHandler still applies either way.
32
+ */
33
+ callerPermissions?: number;
26
34
  }
27
35
 
28
36
  export function createServer(options: CreateServerOptions): McpServer {
29
- const { haClient, omadaClient, aiClient, restActions, toolRegistrationMode } = options;
37
+ const { haClient, omadaClient, aiClient, restActions, toolRegistrationMode, callerPermissions } = options;
30
38
  logger.debug('Creating MCP server instance');
31
39
 
32
40
  // Generate instructions based on enabled plugins
@@ -48,15 +56,32 @@ export function createServer(options: CreateServerOptions): McpServer {
48
56
 
49
57
  logger.debug('Server instructions generated', { length: instructions.length });
50
58
 
51
- // Register all tools based on configured clients
52
- registerAllTools({
53
- server,
54
- haClient,
55
- omadaClient,
56
- aiClient,
57
- restActions,
58
- toolRegistrationMode,
59
- });
59
+ // Register all tools based on configured clients, capturing each RegisteredTool
60
+ // handle so tools the caller can never use can be hidden afterwards.
61
+ const registeredTools = new Map<string, RegisteredTool>();
62
+ const originalRegisterTool = server.registerTool;
63
+ server.registerTool = ((...args: Parameters<McpServer['registerTool']>) => {
64
+ const tool = (originalRegisterTool as (...a: unknown[]) => RegisteredTool).apply(server, args);
65
+ registeredTools.set(args[0], tool);
66
+ return tool;
67
+ }) as McpServer['registerTool'];
68
+ try {
69
+ registerAllTools({
70
+ server,
71
+ haClient,
72
+ omadaClient,
73
+ aiClient,
74
+ restActions,
75
+ toolRegistrationMode,
76
+ });
77
+ } finally {
78
+ server.registerTool = originalRegisterTool;
79
+ }
80
+
81
+ if (callerPermissions !== undefined) {
82
+ const hidden = hideUnpermittedTools(registeredTools, callerPermissions);
83
+ logger.debug('Filtered tools by caller permissions', { visible: registeredTools.size - hidden, hidden });
84
+ }
60
85
 
61
86
  // Register Home Assistant resources (if HA client provided)
62
87
  if (haClient) {
@@ -67,3 +92,20 @@ export function createServer(options: CreateServerOptions): McpServer {
67
92
 
68
93
  return server;
69
94
  }
95
+
96
+ /**
97
+ * Disable every tool the caller cannot use (it lacks the tool's static permission, or the
98
+ * tool's visibility rule — e.g. omada_read's per-path one — finds nothing usable). Disabled
99
+ * tools are omitted from `tools/list`, so an LLM client only plans with tools it can use.
100
+ * Returns the number of tools hidden.
101
+ */
102
+ export function hideUnpermittedTools(tools: Map<string, RegisteredTool>, callerPermissions: number): number {
103
+ let hidden = 0;
104
+ for (const [name, tool] of tools) {
105
+ if (!isToolVisibleTo(name, callerPermissions)) {
106
+ tool.disable();
107
+ hidden++;
108
+ }
109
+ }
110
+ return hidden;
111
+ }
@@ -12,6 +12,7 @@ import type { LocalAIClient } from '../localAI/index.js';
12
12
  import type { OmadaClient } from '../omadaClient/index.js';
13
13
  import { logger } from '../utils/logger.js';
14
14
  import { createServer } from './common.js';
15
+ import type { AuthenticatedRequest } from './auth.js';
15
16
 
16
17
  interface StreamTransportState {
17
18
  transport: StreamableHTTPServerTransport;
@@ -92,7 +93,7 @@ export function buildTransportSecurityOptions(config: EnvironmentConfig): Transp
92
93
  * Creates a Streamable HTTP transport
93
94
  * This implements the MCP protocol version 2025-03-26
94
95
  */
95
- export function createStreamTransport(options: StreamTransportOptions): StreamTransportState {
96
+ export function createStreamTransport(options: StreamTransportOptions, callerPermissions?: number): StreamTransportState {
96
97
  const { haClient, omadaClient, aiClient, config } = options;
97
98
  const mcpServer = createServer({
98
99
  haClient,
@@ -100,6 +101,8 @@ export function createStreamTransport(options: StreamTransportOptions): StreamTr
100
101
  aiClient,
101
102
  restActions: config.restActions,
102
103
  toolRegistrationMode: config.toolRegistrationMode,
104
+ // Hide tools this caller can never use from tools/list (RBAC still enforced per call).
105
+ callerPermissions,
103
106
  });
104
107
 
105
108
  const enableStatefulSessions = config.stateful;
@@ -158,7 +161,9 @@ export async function handleStreamRequest(
158
161
  });
159
162
 
160
163
  // Reuse existing transport if provided, otherwise create new one
161
- const state = existingTransport ?? createStreamTransport(options);
164
+ // The auth middleware attached the caller's mask; a new server lists only the tools it permits.
165
+ const callerPermissions = (req as AuthenticatedRequest).auth?.extra?.permissions as number | undefined;
166
+ const state = existingTransport ?? createStreamTransport(options, callerPermissions);
162
167
 
163
168
  if (!existingTransport) {
164
169
  await state.server.connect(state.transport);
@@ -73,6 +73,45 @@ export function safeSerialize(value: unknown): string {
73
73
  }
74
74
  }
75
75
 
76
+ /**
77
+ * Static permission bit each tool declares via `wrapToolHandler`, keyed by tool
78
+ * name. Lets the server hide tools a caller can never use from `tools/list`.
79
+ * Tools with no static bit (e.g. `omada_read`/`omada_browse`, which gate per
80
+ * resource path) are absent and therefore always listed.
81
+ */
82
+ const toolPermissions = new Map<string, number>();
83
+
84
+ /** The static permission a tool requires, or `undefined` when it declares none. */
85
+ export function getToolRequiredPermission(name: string): number | undefined {
86
+ return toolPermissions.get(name);
87
+ }
88
+
89
+ /**
90
+ * Visibility rules for tools that gate per call rather than with one static bit
91
+ * (e.g. `omada_read`, whose permission depends on the resource path). The rule
92
+ * answers "can this caller use the tool at all?".
93
+ */
94
+ const toolVisibilityRules = new Map<string, (callerPermissions: number) => boolean>();
95
+
96
+ /** Declare when a tool without a static permission should be listed to a caller. */
97
+ export function setToolVisibilityRule(name: string, isUsable: (callerPermissions: number) => boolean): void {
98
+ toolVisibilityRules.set(name, isUsable);
99
+ }
100
+
101
+ /**
102
+ * Whether `tools/list` should offer `name` to a caller: false when the caller lacks
103
+ * the tool's static permission, or when its visibility rule says the caller can use
104
+ * none of it. Tools with neither stay visible (their handlers still enforce RBAC).
105
+ */
106
+ export function isToolVisibleTo(name: string, callerPermissions: number): boolean {
107
+ const required = toolPermissions.get(name);
108
+ if (required !== undefined && !hasPermission(callerPermissions, required)) {
109
+ return false;
110
+ }
111
+ const rule = toolVisibilityRules.get(name);
112
+ return rule ? rule(callerPermissions) : true;
113
+ }
114
+
76
115
  /**
77
116
  * Wrap a tool handler with logging, error handling, and permission checking
78
117
  * Includes session ID and args in logs for debugging
@@ -86,6 +125,9 @@ export function wrapToolHandler<T>(
86
125
  handler: (args: T, extra: ToolExtra) => Promise<CallToolResult>,
87
126
  requiredPermission?: number
88
127
  ): (args: T, extra: ToolExtra) => Promise<CallToolResult> {
128
+ if (requiredPermission !== undefined) {
129
+ toolPermissions.set(name, requiredPermission);
130
+ }
89
131
  return async (args: T, extra: ToolExtra): Promise<CallToolResult> => {
90
132
  const sessionId = extra.sessionId ?? 'unknown-session';
91
133
  // Extract permissions from MCP SDK authInfo.extra.permissions.
@@ -5,6 +5,7 @@
5
5
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import type { HaClient } from '../../haClient/index.js';
7
7
  import { controlClimateSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
8
+ import { entityNotFoundResult } from './entityGuard.js';
8
9
  import type { z } from 'zod';
9
10
 
10
11
  type ControlClimateArgs = z.infer<typeof controlClimateSchema>;
@@ -17,6 +18,8 @@ export function registerControlClimateTool(server: McpServer, client: HaClient):
17
18
  inputSchema: controlClimateSchema.shape,
18
19
  },
19
20
  wrapToolHandler('controlClimate', async (args: ControlClimateArgs) => {
21
+ const missing = await entityNotFoundResult(client, args.entity_id);
22
+ if (missing) return missing;
20
23
  const results: unknown[] = [];
21
24
 
22
25
  if (args.hvac_mode !== undefined) {
@@ -5,6 +5,7 @@
5
5
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import type { HaClient } from '../../haClient/index.js';
7
7
  import { controlCoverSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
8
+ import { entityNotFoundResult } from './entityGuard.js';
8
9
  import type { z } from 'zod';
9
10
 
10
11
  type ControlCoverArgs = z.infer<typeof controlCoverSchema>;
@@ -17,6 +18,8 @@ export function registerControlCoverTool(server: McpServer, client: HaClient): v
17
18
  inputSchema: controlCoverSchema.shape,
18
19
  },
19
20
  wrapToolHandler('controlCover', async (args: ControlCoverArgs) => {
21
+ const missing = await entityNotFoundResult(client, args.entity_id);
22
+ if (missing) return missing;
20
23
  const actionMap: Record<string, string> = {
21
24
  open: 'open_cover',
22
25
  close: 'close_cover',
@@ -5,6 +5,7 @@
5
5
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import type { HaClient } from '../../haClient/index.js';
7
7
  import { controlFanSchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
8
+ import { entityNotFoundResult } from './entityGuard.js';
8
9
  import type { z } from 'zod';
9
10
 
10
11
  type ControlFanArgs = z.infer<typeof controlFanSchema>;
@@ -17,6 +18,8 @@ export function registerControlFanTool(server: McpServer, client: HaClient): voi
17
18
  inputSchema: controlFanSchema.shape,
18
19
  },
19
20
  wrapToolHandler('controlFan', async (args: ControlFanArgs) => {
21
+ const missing = await entityNotFoundResult(client, args.entity_id);
22
+ if (missing) return missing;
20
23
  let service: string = args.action;
21
24
  const serviceData: Record<string, unknown> = {};
22
25