@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.4.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/.env.example +1 -1
- package/README.md +24 -2
- package/dist/omadaClient/accessControl.d.ts +290 -0
- package/dist/omadaClient/accessControl.js +509 -0
- package/dist/omadaClient/accessControl.js.map +1 -0
- package/dist/omadaClient/client.d.ts +6 -1
- package/dist/omadaClient/client.js +12 -1
- package/dist/omadaClient/client.js.map +1 -1
- package/dist/omadaClient/diagnose.d.ts +68 -0
- package/dist/omadaClient/diagnose.js +164 -0
- package/dist/omadaClient/diagnose.js.map +1 -0
- package/dist/omadaClient/index.d.ts +9 -0
- package/dist/omadaClient/index.js +19 -0
- package/dist/omadaClient/index.js.map +1 -1
- package/dist/omadaClient/macGroups.d.ts +58 -0
- package/dist/omadaClient/macGroups.js +110 -0
- package/dist/omadaClient/macGroups.js.map +1 -0
- package/dist/omadaClient/serialize.d.ts +13 -0
- package/dist/omadaClient/serialize.js +29 -0
- package/dist/omadaClient/serialize.js.map +1 -0
- package/dist/omadaClient/timeRange.d.ts +41 -0
- package/dist/omadaClient/timeRange.js +109 -0
- package/dist/omadaClient/timeRange.js.map +1 -0
- package/dist/tools/infra/actions.d.ts +1 -1
- package/dist/tools/infra/actions.js +1 -1
- package/dist/tools/omada/accessControl.d.ts +151 -0
- package/dist/tools/omada/accessControl.js +158 -0
- package/dist/tools/omada/accessControl.js.map +1 -0
- package/dist/tools/omada/index.js +9 -0
- package/dist/tools/omada/index.js.map +1 -1
- package/dist/tools/omada/macGroups.d.ts +19 -0
- package/dist/tools/omada/macGroups.js +77 -0
- package/dist/tools/omada/macGroups.js.map +1 -0
- package/dist/tools/omada/namespace.js +16 -0
- package/dist/tools/omada/namespace.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/.codex/review-rules.md +0 -16
- package/.dockerignore +0 -8
- package/.gitattributes +0 -30
- package/.github/workflows/ci.yml +0 -193
- package/AGENTS.md +0 -389
- package/Dockerfile +0 -50
- package/docker-compose.yml +0 -23
- package/docs/CHANGELOG.md +0 -341
- package/docs/COVERAGE.md +0 -219
- package/docs/QUICK_START.md +0 -240
- package/eslint.config.js +0 -33
- package/n8n/ai-agent-safety-monitor.json +0 -355
- package/openapi.json +0 -1259
- package/scripts/check-version-sync.mjs +0 -69
- package/scripts/generate-jwt.ts +0 -121
- package/src/config.ts +0 -440
- package/src/haClient/automations.ts +0 -261
- package/src/haClient/calendars.ts +0 -94
- package/src/haClient/config.ts +0 -52
- package/src/haClient/devices.ts +0 -653
- package/src/haClient/entities.ts +0 -128
- package/src/haClient/events.ts +0 -689
- package/src/haClient/history.ts +0 -73
- package/src/haClient/index.ts +0 -336
- package/src/haClient/request.ts +0 -142
- package/src/haClient/services.ts +0 -45
- package/src/haClient/states.ts +0 -60
- package/src/haClient/updates.ts +0 -58
- package/src/haClient/wsCommand.ts +0 -44
- package/src/index.ts +0 -164
- package/src/localAI/index.ts +0 -88
- package/src/localAI/prompts.ts +0 -50
- package/src/localAI/providers/base.ts +0 -120
- package/src/localAI/providers/ollama.ts +0 -78
- package/src/localAI/providers/openai.ts +0 -86
- package/src/localAI/types.ts +0 -67
- package/src/omadaClient/auth.ts +0 -178
- package/src/omadaClient/client.ts +0 -337
- package/src/omadaClient/device.ts +0 -118
- package/src/omadaClient/index.ts +0 -310
- package/src/omadaClient/network.ts +0 -284
- package/src/omadaClient/request.ts +0 -365
- package/src/omadaClient/security.ts +0 -51
- package/src/omadaClient/site.ts +0 -344
- package/src/ownerQuestions/service.ts +0 -536
- package/src/permissions/index.ts +0 -143
- package/src/resources/index.ts +0 -213
- package/src/server/auth.ts +0 -175
- package/src/server/chatFace.ts +0 -789
- package/src/server/chatSlice.ts +0 -57
- package/src/server/common.ts +0 -170
- package/src/server/eventSubscription.ts +0 -343
- package/src/server/http.ts +0 -626
- package/src/server/instructions.ts +0 -97
- package/src/server/rateLimiter.ts +0 -227
- package/src/server/stdio.ts +0 -41
- package/src/server/stream.ts +0 -205
- package/src/tools/ai/analyzeSensors.ts +0 -33
- package/src/tools/ai/index.ts +0 -20
- package/src/tools/common.ts +0 -442
- package/src/tools/homeassistant/askOwner.ts +0 -133
- package/src/tools/homeassistant/automationControls.ts +0 -66
- package/src/tools/homeassistant/calendar.ts +0 -97
- package/src/tools/homeassistant/callService.ts +0 -25
- package/src/tools/homeassistant/controlClimate.ts +0 -68
- package/src/tools/homeassistant/controlCover.ts +0 -48
- package/src/tools/homeassistant/controlFan.ts +0 -44
- package/src/tools/homeassistant/controlLight.ts +0 -38
- package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
- package/src/tools/homeassistant/createAutomation.ts +0 -35
- package/src/tools/homeassistant/deleteAutomation.ts +0 -24
- package/src/tools/homeassistant/entityAction.ts +0 -35
- package/src/tools/homeassistant/entityGuard.ts +0 -28
- package/src/tools/homeassistant/getAllSensors.ts +0 -46
- package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
- package/src/tools/homeassistant/getDomainSummary.ts +0 -29
- package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
- package/src/tools/homeassistant/getHistory.ts +0 -24
- package/src/tools/homeassistant/getState.ts +0 -24
- package/src/tools/homeassistant/getStates.ts +0 -46
- package/src/tools/homeassistant/getVersion.ts +0 -21
- package/src/tools/homeassistant/index.ts +0 -121
- package/src/tools/homeassistant/listAutomations.ts +0 -35
- package/src/tools/homeassistant/listEntities.ts +0 -44
- package/src/tools/homeassistant/listPersons.ts +0 -46
- package/src/tools/homeassistant/sceneAndScript.ts +0 -53
- package/src/tools/homeassistant/searchEntities.ts +0 -58
- package/src/tools/homeassistant/sendNotification.ts +0 -179
- package/src/tools/homeassistant/systemTools.ts +0 -52
- package/src/tools/homeassistant/triggerAutomation.ts +0 -24
- package/src/tools/index.ts +0 -78
- package/src/tools/infra/actions.ts +0 -95
- package/src/tools/infra/index.ts +0 -138
- package/src/tools/omada/blockClient.ts +0 -28
- package/src/tools/omada/cyclePoePort.ts +0 -32
- package/src/tools/omada/deleteClient.ts +0 -27
- package/src/tools/omada/disableClientRateLimit.ts +0 -23
- package/src/tools/omada/getClient.ts +0 -14
- package/src/tools/omada/getDevice.ts +0 -14
- package/src/tools/omada/getFirewallSetting.ts +0 -14
- package/src/tools/omada/getInternetInfo.ts +0 -14
- package/src/tools/omada/getLanNetworkList.ts +0 -14
- package/src/tools/omada/getLanProfileList.ts +0 -14
- package/src/tools/omada/getPortForwardingStatus.ts +0 -26
- package/src/tools/omada/getRateLimitProfiles.ts +0 -14
- package/src/tools/omada/getSiteNtpStatus.ts +0 -14
- package/src/tools/omada/getSsidDetail.ts +0 -22
- package/src/tools/omada/getSsidList.ts +0 -21
- package/src/tools/omada/getSwitchStackDetail.ts +0 -14
- package/src/tools/omada/getThreatList.ts +0 -42
- package/src/tools/omada/getWlanGroupList.ts +0 -14
- package/src/tools/omada/graph.ts +0 -187
- package/src/tools/omada/index.ts +0 -153
- package/src/tools/omada/listBlockedClients.ts +0 -17
- package/src/tools/omada/listClients.ts +0 -14
- package/src/tools/omada/listClientsActivity.ts +0 -25
- package/src/tools/omada/listClientsPastConnections.ts +0 -44
- package/src/tools/omada/listDevices.ts +0 -14
- package/src/tools/omada/listDevicesStats.ts +0 -41
- package/src/tools/omada/listMostActiveClients.ts +0 -14
- package/src/tools/omada/listSites.ts +0 -18
- package/src/tools/omada/namespace.ts +0 -1164
- package/src/tools/omada/searchDevices.ts +0 -20
- package/src/tools/omada/setClientRateLimit.ts +0 -26
- package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
- package/src/tools/omada/setSiteNtpServers.ts +0 -36
- package/src/tools/omada/setSsidEnabled.ts +0 -38
- package/src/tools/omada/unblockClient.ts +0 -26
- package/src/tools/types.ts +0 -11
- package/src/tools/utils/pagination-schema.ts +0 -27
- package/src/types/ai/index.ts +0 -11
- package/src/types/ai/types.ts +0 -67
- package/src/types/homeassistant/automations.ts +0 -43
- package/src/types/homeassistant/calendars.ts +0 -26
- package/src/types/homeassistant/config.ts +0 -14
- package/src/types/homeassistant/entities.ts +0 -14
- package/src/types/homeassistant/errors.ts +0 -33
- package/src/types/homeassistant/haConfig.ts +0 -7
- package/src/types/homeassistant/history.ts +0 -13
- package/src/types/homeassistant/index.ts +0 -46
- package/src/types/homeassistant/request.ts +0 -6
- package/src/types/homeassistant/serviceCall.ts +0 -18
- package/src/types/homeassistant/states.ts +0 -58
- package/src/types/homeassistant/updates.ts +0 -28
- package/src/types/index.ts +0 -12
- package/src/types/omada/activeClientInfo.ts +0 -17
- package/src/types/omada/clientActivity.ts +0 -32
- package/src/types/omada/clientBlockStatus.ts +0 -11
- package/src/types/omada/clientPastConnection.ts +0 -82
- package/src/types/omada/index.ts +0 -14
- package/src/types/omada/omadaApiResponse.ts +0 -5
- package/src/types/omada/omadaClientInfo.ts +0 -11
- package/src/types/omada/omadaDeviceInfo.ts +0 -11
- package/src/types/omada/omadaDeviceStats.ts +0 -31
- package/src/types/omada/omadaSiteSummary.ts +0 -5
- package/src/types/omada/oswStackDetail.ts +0 -11
- package/src/types/omada/paginatedResult.ts +0 -6
- package/src/types/omada/rateLimitProfile.ts +0 -58
- package/src/types/omada/threatInfo.ts +0 -38
- package/src/types/omada/tokenResult.ts +0 -6
- package/src/types.ts +0 -2
- package/src/utils/config-validations.ts +0 -68
- package/src/utils/jwt.ts +0 -112
- package/src/utils/logger.ts +0 -121
- package/src/utils/plainText.ts +0 -48
- package/src/utils/redact.ts +0 -59
- package/src/utils/sanitizeError.ts +0 -18
- package/src/utils/tlsDispatcher.ts +0 -69
- package/src/version.ts +0 -5
- package/tests/bdd/automation-creation.spec.ts +0 -214
- package/tests/bdd/automation-management.spec.ts +0 -267
- package/tests/bdd/calendar.spec.ts +0 -190
- package/tests/bdd/configuration.spec.ts +0 -283
- package/tests/bdd/device-control.spec.ts +0 -344
- package/tests/bdd/entity-listing.spec.ts +0 -208
- package/tests/bdd/entity-search.spec.ts +0 -183
- package/tests/bdd/history-retrieval.spec.ts +0 -195
- package/tests/bdd/notifications.spec.ts +0 -238
- package/tests/bdd/service-calls.spec.ts +0 -144
- package/tests/bdd/state-retrieval.spec.ts +0 -204
- package/tests/bench/hotpaths.bench.ts +0 -68
- package/tests/config.test.ts +0 -355
- package/tests/features/automation-creation.feature +0 -36
- package/tests/features/automation-management.feature +0 -54
- package/tests/features/calendar.feature +0 -37
- package/tests/features/configuration.feature +0 -70
- package/tests/features/device-control.feature +0 -100
- package/tests/features/entity-listing.feature +0 -51
- package/tests/features/entity-search.feature +0 -44
- package/tests/features/history-retrieval.feature +0 -38
- package/tests/features/notifications.feature +0 -37
- package/tests/features/service-calls.feature +0 -35
- package/tests/features/state-retrieval.feature +0 -37
- package/tests/fixtures/openapi-default-slice.json +0 -290
- package/tests/haClient/automationTrace.test.ts +0 -59
- package/tests/haClient/automations.test.ts +0 -149
- package/tests/haClient/config.test.ts +0 -117
- package/tests/haClient/entities.test.ts +0 -188
- package/tests/haClient/events.test.ts +0 -455
- package/tests/haClient/eventsTls.test.ts +0 -52
- package/tests/haClient/history.test.ts +0 -228
- package/tests/haClient/index.test.ts +0 -243
- package/tests/haClient/request.test.ts +0 -217
- package/tests/haClient/services.test.ts +0 -120
- package/tests/haClient/states.test.ts +0 -119
- package/tests/haClient/updates.test.ts +0 -193
- package/tests/index.test.ts +0 -160
- package/tests/integration/homeassistant.test.ts +0 -144
- package/tests/integration/omada.test.ts +0 -167
- package/tests/integration/server.test.ts +0 -151
- package/tests/localAI/index.test.ts +0 -177
- package/tests/localAI/ollama.test.ts +0 -187
- package/tests/localAI/openai.test.ts +0 -257
- package/tests/omadaClient/actions.test.ts +0 -161
- package/tests/omadaClient/clientBlocks.test.ts +0 -170
- package/tests/omadaClient/ntp.test.ts +0 -381
- package/tests/omadaClient/site.test.ts +0 -37
- package/tests/ownerQuestions/service.test.ts +0 -499
- package/tests/ownerQuestions/tools.test.ts +0 -186
- package/tests/permissions/index.test.ts +0 -217
- package/tests/resources/index.test.ts +0 -288
- package/tests/scripts/generateJwt.test.ts +0 -44
- package/tests/security/accessControl.test.ts +0 -223
- package/tests/security/authentication.test.ts +0 -117
- package/tests/security/configuration.test.ts +0 -170
- package/tests/security/cryptoTransport.test.ts +0 -172
- package/tests/security/dependencies.test.ts +0 -152
- package/tests/security/injection.test.ts +0 -117
- package/tests/security/insecureDesign.test.ts +0 -121
- package/tests/security/logging.test.ts +0 -131
- package/tests/security/resourceConsumption.test.ts +0 -142
- package/tests/server/auth.test.ts +0 -249
- package/tests/server/chatFace.test.ts +0 -320
- package/tests/server/common.test.ts +0 -84
- package/tests/server/http.test.ts +0 -561
- package/tests/server/instructions.test.ts +0 -152
- package/tests/server/memoizeJsonSchema.test.ts +0 -71
- package/tests/server/stdio.test.ts +0 -99
- package/tests/server/stream.test.ts +0 -285
- package/tests/server/toolVisibility.http.test.ts +0 -111
- package/tests/server/toolVisibility.test.ts +0 -129
- package/tests/tools/ai/analyzeSensors.test.ts +0 -227
- package/tests/tools/common.test.ts +0 -123
- package/tests/tools/homeassistant/handlers.test.ts +0 -872
- package/tests/tools/infra/invokeAction.test.ts +0 -280
- package/tests/tools/omada/graph.test.ts +0 -385
- package/tests/tools/omada/handlers.test.ts +0 -897
- package/tests/tools/omada/namespace.test.ts +0 -483
- package/tests/tools/registry.test.ts +0 -168
- package/tests/utils/config-validations.test.ts +0 -352
- package/tests/utils/jwt.test.ts +0 -115
- package/tests/utils/logger.test.ts +0 -327
- package/tests/utils/redact.test.ts +0 -105
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -38
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat face (v1.7) — the unified binding registry that drives REST dispatch,
|
|
3
|
-
* per-route RBAC, and the generated OpenAPI spec.
|
|
4
|
-
*
|
|
5
|
-
* The two load-bearing properties under test:
|
|
6
|
-
* 1. BYTE-COMPATIBILITY: with no MCP_CHAT_TOOLS slice, the generated spec is
|
|
7
|
-
* identical to the pre-v1.7 hand-written one (golden fixture) — the NAS
|
|
8
|
-
* deployment must see zero difference from this refactor.
|
|
9
|
-
* 2. ADMIN-NEVER-CHAT: no chat binding may carry the ADMIN bit or expose
|
|
10
|
-
* invokeAction — the deploy trigger stays off the prompt-injectable
|
|
11
|
-
* surface structurally.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
15
|
-
import { readFileSync } from 'node:fs';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
import { dirname, join } from 'node:path';
|
|
18
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
LEGACY_BINDINGS,
|
|
22
|
-
CHAT_TOOL_BINDINGS,
|
|
23
|
-
resolveChatBindings,
|
|
24
|
-
buildOpenApiSpec,
|
|
25
|
-
dispatchChatRequest,
|
|
26
|
-
requiredPermissionFor,
|
|
27
|
-
zodToOpenApiSchema,
|
|
28
|
-
} from '../../src/server/chatFace.js';
|
|
29
|
-
import { parseChatTools, CHAT_CATEGORIES } from '../../src/server/chatSlice.js';
|
|
30
|
-
import { Permission } from '../../src/permissions/index.js';
|
|
31
|
-
import { VERSION } from '../../src/version.js';
|
|
32
|
-
import type { HaClient } from '../../src/haClient/index.js';
|
|
33
|
-
import type { OmadaClient } from '../../src/omadaClient/index.js';
|
|
34
|
-
import { z } from 'zod';
|
|
35
|
-
|
|
36
|
-
const fixtureDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'fixtures');
|
|
37
|
-
|
|
38
|
-
const LEGACY_OPERATION_IDS = [
|
|
39
|
-
'getStates',
|
|
40
|
-
'getState',
|
|
41
|
-
'callService',
|
|
42
|
-
'getAllSensors',
|
|
43
|
-
'getEntitiesByDomain',
|
|
44
|
-
'searchEntities',
|
|
45
|
-
'getHistory',
|
|
46
|
-
'getVersion',
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
describe('chat face — spec byte-compatibility (golden file)', () => {
|
|
50
|
-
it('default slice renders the pre-v1.7 spec exactly (modulo version)', () => {
|
|
51
|
-
const golden = JSON.parse(
|
|
52
|
-
readFileSync(join(fixtureDir, 'openapi-default-slice.json'), 'utf8')
|
|
53
|
-
) as { info: { version: string } };
|
|
54
|
-
// The fixture was captured at its release version; only the version string
|
|
55
|
-
// may differ across releases — everything else must match to the byte.
|
|
56
|
-
golden.info.version = VERSION;
|
|
57
|
-
|
|
58
|
-
const generated = JSON.parse(
|
|
59
|
-
JSON.stringify(buildOpenApiSpec('http://__BASE_URL__', LEGACY_BINDINGS))
|
|
60
|
-
) as unknown;
|
|
61
|
-
|
|
62
|
-
expect(generated).toEqual(golden);
|
|
63
|
-
// Belt-and-braces: key ORDER matters for byte-compat, so compare strings too.
|
|
64
|
-
expect(JSON.stringify(generated)).toBe(JSON.stringify(golden));
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('chat face — security invariants', () => {
|
|
69
|
-
const allBindings = [...LEGACY_BINDINGS, ...CHAT_TOOL_BINDINGS];
|
|
70
|
-
|
|
71
|
-
it('no chat binding carries the ADMIN bit (invokeAction can never be chat-faced)', () => {
|
|
72
|
-
for (const binding of allBindings) {
|
|
73
|
-
expect(
|
|
74
|
-
binding.permission & Permission.ADMIN,
|
|
75
|
-
`${binding.operationId} must not require (= be granted via) ADMIN on the chat face`
|
|
76
|
-
).toBe(0);
|
|
77
|
-
expect(binding.operationId).not.toBe('invokeAction');
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('omada_cyclePoePort is deliberately not chat-eligible', () => {
|
|
82
|
-
expect(allBindings.map((b) => b.operationId)).not.toContain('omada_cyclePoePort');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('every binding declares a known category and a non-zero permission', () => {
|
|
86
|
-
for (const binding of allBindings) {
|
|
87
|
-
expect(CHAT_CATEGORIES).toContain(binding.category);
|
|
88
|
-
expect(binding.permission).toBeGreaterThan(0);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('parseChatTools', () => {
|
|
94
|
-
it('returns undefined for unset/blank (legacy default)', () => {
|
|
95
|
-
expect(parseChatTools(undefined)).toBeUndefined();
|
|
96
|
-
expect(parseChatTools('')).toBeUndefined();
|
|
97
|
-
expect(parseChatTools(' ')).toBeUndefined();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('parses categories with r/w/rw access', () => {
|
|
101
|
-
const slice = parseChatTools('ha-core:rw, ha-history:r,omada-read:r');
|
|
102
|
-
expect(slice?.get('ha-core')).toEqual(new Set(['read', 'write']));
|
|
103
|
-
expect(slice?.get('ha-history')).toEqual(new Set(['read']));
|
|
104
|
-
expect(slice?.get('omada-read')).toEqual(new Set(['read']));
|
|
105
|
-
expect(slice?.has('omada-write')).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('fails loudly on unknown categories and malformed access', () => {
|
|
109
|
-
expect(() => parseChatTools('infra:rw')).toThrow(/Invalid MCP_CHAT_TOOLS category/);
|
|
110
|
-
expect(() => parseChatTools('ha-core')).toThrow(/Invalid MCP_CHAT_TOOLS access/);
|
|
111
|
-
expect(() => parseChatTools('ha-core:x')).toThrow(/Invalid MCP_CHAT_TOOLS access/);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('resolveChatBindings', () => {
|
|
116
|
-
const haClient = {} as HaClient;
|
|
117
|
-
const omadaClient = {} as OmadaClient;
|
|
118
|
-
|
|
119
|
-
it('unset slice -> exactly the legacy 8 (zero-surprise upgrade)', () => {
|
|
120
|
-
const bindings = resolveChatBindings(undefined, { haClient, omadaClient });
|
|
121
|
-
expect(bindings.map((b) => b.operationId)).toEqual(LEGACY_OPERATION_IDS);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('slice governs both legacy and tool bindings by category:access', () => {
|
|
125
|
-
const bindings = resolveChatBindings(parseChatTools('ha-core:r'), { haClient, omadaClient });
|
|
126
|
-
const ids = bindings.map((b) => b.operationId);
|
|
127
|
-
// ha-core reads, but NOT callService (write), NOT getHistory (ha-history)
|
|
128
|
-
expect(ids).toContain('getStates');
|
|
129
|
-
expect(ids).toContain('getDomainSummary'); // tool binding, ha-core:read
|
|
130
|
-
expect(ids).not.toContain('callService');
|
|
131
|
-
expect(ids).not.toContain('getHistory');
|
|
132
|
-
expect(ids).not.toContain('omada_browse');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('omada categories activate the graph + SSID tools', () => {
|
|
136
|
-
const bindings = resolveChatBindings(parseChatTools('omada-read:r,omada-write:w'), {
|
|
137
|
-
haClient,
|
|
138
|
-
omadaClient,
|
|
139
|
-
});
|
|
140
|
-
const ids = bindings.map((b) => b.operationId);
|
|
141
|
-
expect(ids).toEqual(['omada_browse', 'omada_read', 'omada_setSsidEnabled']);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('omada bindings are dropped from the face when no Omada client is configured', () => {
|
|
145
|
-
const bindings = resolveChatBindings(parseChatTools('omada-read:r'), { haClient });
|
|
146
|
-
expect(bindings).toEqual([]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('active omada tools appear in the generated spec as /api/tools/* paths', () => {
|
|
150
|
-
const bindings = resolveChatBindings(parseChatTools('ha-core:rw,ha-history:r,omada-read:r'), {
|
|
151
|
-
haClient,
|
|
152
|
-
omadaClient,
|
|
153
|
-
});
|
|
154
|
-
const spec = buildOpenApiSpec('http://x', bindings) as { paths: Record<string, unknown> };
|
|
155
|
-
expect(Object.keys(spec.paths)).toContain('/api/tools/omada_browse');
|
|
156
|
-
expect(Object.keys(spec.paths)).toContain('/api/tools/omada_read');
|
|
157
|
-
expect(Object.keys(spec.paths)).toContain('/api/states');
|
|
158
|
-
expect(Object.keys(spec.paths)).not.toContain('/api/tools/omada_setSsidEnabled');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe('requiredPermissionFor', () => {
|
|
163
|
-
it('mirrors the MCP twin permissions on legacy routes', () => {
|
|
164
|
-
expect(requiredPermissionFor(LEGACY_BINDINGS, 'GET', '/api/states')).toBe(Permission.QUERY);
|
|
165
|
-
expect(requiredPermissionFor(LEGACY_BINDINGS, 'GET', '/api/states/light.x')).toBe(Permission.QUERY);
|
|
166
|
-
expect(requiredPermissionFor(LEGACY_BINDINGS, 'POST', '/api/services/light/turn_on')).toBe(
|
|
167
|
-
Permission.CONTROL
|
|
168
|
-
);
|
|
169
|
-
expect(requiredPermissionFor(LEGACY_BINDINGS, 'GET', '/api/nope')).toBeNull();
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// Dispatch behavior
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
function mockReq(method: string, permissions: number | undefined): IncomingMessage {
|
|
178
|
-
return {
|
|
179
|
-
method,
|
|
180
|
-
headers: { host: 'test' },
|
|
181
|
-
...(permissions !== undefined ? { auth: { extra: { permissions } } } : {}),
|
|
182
|
-
} as unknown as IncomingMessage;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function makeDispatch(overrides: {
|
|
186
|
-
bindings: Parameters<typeof dispatchChatRequest>[3]['bindings'];
|
|
187
|
-
deps?: Parameters<typeof dispatchChatRequest>[3]['deps'];
|
|
188
|
-
body?: unknown;
|
|
189
|
-
}) {
|
|
190
|
-
const sent: { status?: number; body?: unknown } = {};
|
|
191
|
-
const sendJson = vi.fn((_res: ServerResponse, status: number, body: unknown) => {
|
|
192
|
-
sent.status = status;
|
|
193
|
-
sent.body = body;
|
|
194
|
-
});
|
|
195
|
-
const run = (req: IncomingMessage, url: string) =>
|
|
196
|
-
dispatchChatRequest(req, {} as ServerResponse, url, {
|
|
197
|
-
bindings: overrides.bindings,
|
|
198
|
-
deps: overrides.deps ?? {},
|
|
199
|
-
parseBody: () => Promise.resolve(overrides.body),
|
|
200
|
-
sendJson,
|
|
201
|
-
});
|
|
202
|
-
return { run, sent };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
describe('dispatchChatRequest', () => {
|
|
206
|
-
it('404s unknown routes without touching a client', async () => {
|
|
207
|
-
const { run, sent } = makeDispatch({ bindings: LEGACY_BINDINGS });
|
|
208
|
-
await run(mockReq('GET', 0xff), '/api/definitely-not-a-route');
|
|
209
|
-
expect(sent.status).toBe(404);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('fails closed: missing permission mask -> 403', async () => {
|
|
213
|
-
const { run, sent } = makeDispatch({
|
|
214
|
-
bindings: LEGACY_BINDINGS,
|
|
215
|
-
deps: { haClient: {} as HaClient },
|
|
216
|
-
});
|
|
217
|
-
await run(mockReq('GET', undefined), '/api/states');
|
|
218
|
-
expect(sent.status).toBe(403);
|
|
219
|
-
expect((sent.body as { error: string }).error).toBe('Forbidden');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('503s when a required client is not configured', async () => {
|
|
223
|
-
const { run, sent } = makeDispatch({ bindings: LEGACY_BINDINGS, deps: {} });
|
|
224
|
-
await run(mockReq('GET', 0xff), '/api/states');
|
|
225
|
-
expect(sent.status).toBe(503);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('runs a legacy handler with decoded path params', async () => {
|
|
229
|
-
const haClient = {
|
|
230
|
-
getState: vi.fn().mockResolvedValue({ entity_id: 'light.living room', state: 'on' }),
|
|
231
|
-
} as unknown as HaClient;
|
|
232
|
-
const { run, sent } = makeDispatch({ bindings: LEGACY_BINDINGS, deps: { haClient } });
|
|
233
|
-
await run(mockReq('GET', Permission.QUERY), '/api/states/light.living%20room');
|
|
234
|
-
expect(sent.status).toBe(200);
|
|
235
|
-
expect((haClient as unknown as { getState: ReturnType<typeof vi.fn> }).getState).toHaveBeenCalledWith(
|
|
236
|
-
'light.living room'
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('dispatches a chat tool THROUGH the shared MCP handler (single source of truth)', async () => {
|
|
241
|
-
const omadaClient = {
|
|
242
|
-
setSsidEnabled: vi.fn().mockResolvedValue({ ssid: 'guest', enabled: false, applied: true }),
|
|
243
|
-
} as unknown as OmadaClient;
|
|
244
|
-
const bindings = resolveChatBindings(parseChatTools('omada-write:w'), { omadaClient });
|
|
245
|
-
|
|
246
|
-
const { run, sent } = makeDispatch({
|
|
247
|
-
bindings,
|
|
248
|
-
deps: { omadaClient },
|
|
249
|
-
body: { ssid: 'guest', enabled: false },
|
|
250
|
-
});
|
|
251
|
-
await run(mockReq('POST', Permission.CONTROL), '/api/tools/omada_setSsidEnabled');
|
|
252
|
-
|
|
253
|
-
expect(sent.status).toBe(200);
|
|
254
|
-
expect(sent.body).toMatchObject({ ssid: 'guest', enabled: false, applied: true });
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('rejects invalid tool arguments with 400 (zod validation, same as MCP face)', async () => {
|
|
258
|
-
const omadaClient = { setSsidEnabled: vi.fn() } as unknown as OmadaClient;
|
|
259
|
-
const bindings = resolveChatBindings(parseChatTools('omada-write:w'), { omadaClient });
|
|
260
|
-
|
|
261
|
-
const { run, sent } = makeDispatch({
|
|
262
|
-
bindings,
|
|
263
|
-
deps: { omadaClient },
|
|
264
|
-
body: { enabled: 'yes' }, // missing ssid; enabled wrong type
|
|
265
|
-
});
|
|
266
|
-
await run(mockReq('POST', Permission.CONTROL), '/api/tools/omada_setSsidEnabled');
|
|
267
|
-
|
|
268
|
-
expect(sent.status).toBe(400);
|
|
269
|
-
expect((sent.body as { error: string }).error).toBe('Invalid arguments');
|
|
270
|
-
expect(
|
|
271
|
-
(omadaClient as unknown as { setSsidEnabled: ReturnType<typeof vi.fn> }).setSsidEnabled
|
|
272
|
-
).not.toHaveBeenCalled();
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("maps the shared handler's own permission denial to 403 (defense in depth)", async () => {
|
|
276
|
-
const omadaClient = { setSsidEnabled: vi.fn() } as unknown as OmadaClient;
|
|
277
|
-
// Force the binding past route-level RBAC by dispatching with CONTROL,
|
|
278
|
-
// then strip the bit the inner wrapToolHandler checks — impossible via the
|
|
279
|
-
// real dispatcher (same bit), so simulate a QUERY-only caller on a route
|
|
280
|
-
// whose binding requires QUERY but whose inner handler needs CONTROL:
|
|
281
|
-
// omada_read's per-path permissions provide exactly this split, but the
|
|
282
|
-
// simplest deterministic probe is a caller with route bit only.
|
|
283
|
-
const bindings = resolveChatBindings(parseChatTools('omada-read:r'), { omadaClient });
|
|
284
|
-
const { run, sent } = makeDispatch({
|
|
285
|
-
bindings,
|
|
286
|
-
deps: { omadaClient },
|
|
287
|
-
body: { path: '/sites' }, // resource path requiring more than QUERY (per manifest)
|
|
288
|
-
});
|
|
289
|
-
await run(mockReq('POST', Permission.QUERY), '/api/tools/omada_read');
|
|
290
|
-
// Either the manifest grants /sites to QUERY (200) or denies (403) — the
|
|
291
|
-
// invariant under test is that a denial surfaces as 403, never 200.
|
|
292
|
-
expect([200, 403, 500]).toContain(sent.status);
|
|
293
|
-
if (sent.status === 403) {
|
|
294
|
-
expect(JSON.stringify(sent.body)).toContain('Permission denied');
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
describe('zodToOpenApiSchema', () => {
|
|
300
|
-
it('converts the schema subset used by chat tools', () => {
|
|
301
|
-
const schema = z.object({
|
|
302
|
-
name: z.string().describe('The name'),
|
|
303
|
-
enabled: z.boolean(),
|
|
304
|
-
mode: z.enum(['a', 'b']).optional(),
|
|
305
|
-
count: z.number().int().min(1).optional(),
|
|
306
|
-
path: z.string().default('/'),
|
|
307
|
-
});
|
|
308
|
-
const out = zodToOpenApiSchema(schema) as {
|
|
309
|
-
type: string;
|
|
310
|
-
properties: Record<string, { type?: string; enum?: string[]; default?: unknown }>;
|
|
311
|
-
required?: string[];
|
|
312
|
-
};
|
|
313
|
-
expect(out.type).toBe('object');
|
|
314
|
-
expect(out.properties.name).toEqual({ type: 'string', description: 'The name' });
|
|
315
|
-
expect(out.properties.enabled.type).toBe('boolean');
|
|
316
|
-
expect(out.properties.mode.enum).toEqual(['a', 'b']);
|
|
317
|
-
expect(out.properties.path.default).toBe('/');
|
|
318
|
-
expect(out.required).toEqual(['name', 'enabled']);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server common functionality tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
-
import { createServer } from '../../src/server/common.js';
|
|
7
|
-
|
|
8
|
-
// Mock HaClient
|
|
9
|
-
const mockHaClient = {
|
|
10
|
-
getStates: vi.fn(),
|
|
11
|
-
getState: vi.fn(),
|
|
12
|
-
searchEntities: vi.fn(),
|
|
13
|
-
getEntitiesByDomain: vi.fn(),
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// Mock the tools index
|
|
17
|
-
vi.mock('../../src/tools/index.js', () => ({
|
|
18
|
-
registerAllTools: vi.fn(),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
// Mock the resources index
|
|
22
|
-
vi.mock('../../src/resources/index.js', () => ({
|
|
23
|
-
registerAllResources: vi.fn(),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
describe('createServer', () => {
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
vi.clearAllMocks();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should create MCP server with correct name and version', () => {
|
|
32
|
-
const server = createServer({ haClient: mockHaClient as any });
|
|
33
|
-
|
|
34
|
-
expect(server).toBeDefined();
|
|
35
|
-
// Server should have the correct configuration
|
|
36
|
-
// Note: We can't easily test internal properties, but we can verify it was created
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should register all tools on server creation', async () => {
|
|
40
|
-
const { registerAllTools } = await import('../../src/tools/index.js');
|
|
41
|
-
const server = createServer({ haClient: mockHaClient as any });
|
|
42
|
-
|
|
43
|
-
expect(registerAllTools).toHaveBeenCalledWith({
|
|
44
|
-
server,
|
|
45
|
-
haClient: mockHaClient,
|
|
46
|
-
omadaClient: undefined,
|
|
47
|
-
aiClient: undefined,
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should create server with tools capability', () => {
|
|
52
|
-
const server = createServer({ haClient: mockHaClient as any });
|
|
53
|
-
|
|
54
|
-
// The server should be created successfully
|
|
55
|
-
expect(server).toBeDefined();
|
|
56
|
-
expect(typeof server.connect).toBe('function');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should work without haClient (Omada-only mode)', () => {
|
|
60
|
-
const server = createServer({});
|
|
61
|
-
|
|
62
|
-
expect(server).toBeDefined();
|
|
63
|
-
expect(typeof server.connect).toBe('function');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should accept omadaClient and aiClient', async () => {
|
|
67
|
-
const { registerAllTools } = await import('../../src/tools/index.js');
|
|
68
|
-
const mockOmadaClient = { listSites: vi.fn() };
|
|
69
|
-
const mockAiClient = { analyze: vi.fn() };
|
|
70
|
-
|
|
71
|
-
const server = createServer({
|
|
72
|
-
haClient: mockHaClient as any,
|
|
73
|
-
omadaClient: mockOmadaClient as any,
|
|
74
|
-
aiClient: mockAiClient as any,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(registerAllTools).toHaveBeenCalledWith({
|
|
78
|
-
server,
|
|
79
|
-
haClient: mockHaClient,
|
|
80
|
-
omadaClient: mockOmadaClient,
|
|
81
|
-
aiClient: mockAiClient,
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
});
|