@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,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression: tools/list over the REAL HTTP path is filtered by the caller's token.
|
|
3
|
-
*
|
|
4
|
-
* Drives the production pieces end to end — bearer auth middleware (real HS256
|
|
5
|
-
* JWTs) -> parseBody -> handleStreamRequest -> per-request McpServer — so that
|
|
6
|
-
* removing the request-to-createServer permission forwarding in stream.ts fails
|
|
7
|
-
* here, not just the createServer-level unit tests.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
11
|
-
import { createServer as createHttpServer, type Server } from 'node:http';
|
|
12
|
-
import type { AddressInfo } from 'node:net';
|
|
13
|
-
import { createAuthMiddleware } from '../../src/server/auth.js';
|
|
14
|
-
import { parseBody } from '../../src/server/http.js';
|
|
15
|
-
import { handleStreamRequest } from '../../src/server/stream.js';
|
|
16
|
-
import { createJwt } from '../../src/utils/jwt.js';
|
|
17
|
-
import { parsePermissionsConfig } from '../../src/permissions/index.js';
|
|
18
|
-
import type { EnvironmentConfig } from '../../src/config.js';
|
|
19
|
-
|
|
20
|
-
vi.mock('../../src/utils/logger.js', () => ({
|
|
21
|
-
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
const SECRET = 'test-secret-for-tool-visibility-0123456789';
|
|
25
|
-
const permissions = parsePermissionsConfig(
|
|
26
|
-
JSON.stringify({
|
|
27
|
-
users: [
|
|
28
|
-
{ sub: 'admin', role: 'admin' },
|
|
29
|
-
{ sub: 'operator', role: 'operator' },
|
|
30
|
-
{ sub: 'reader', role: 'readonly' },
|
|
31
|
-
],
|
|
32
|
-
defaultRole: 'NONE',
|
|
33
|
-
})
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// Only the fields the stream path reads; HA client is a registration-only stub.
|
|
37
|
-
const config = { stateful: false, httpAllowedHosts: [] } as unknown as EnvironmentConfig;
|
|
38
|
-
const options = { haClient: {} as never, config };
|
|
39
|
-
|
|
40
|
-
let server: Server;
|
|
41
|
-
let baseUrl: string;
|
|
42
|
-
|
|
43
|
-
beforeAll(async () => {
|
|
44
|
-
const auth = createAuthMiddleware({ method: 'bearer', secret: SECRET, permissions });
|
|
45
|
-
server = createHttpServer(async (req, res) => {
|
|
46
|
-
if (!auth(req, res)) return;
|
|
47
|
-
const body = await parseBody(req);
|
|
48
|
-
await handleStreamRequest(options, req, res, body);
|
|
49
|
-
});
|
|
50
|
-
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
51
|
-
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}/mcp`;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
afterAll(async () => {
|
|
55
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
async function listTools(sub: string): Promise<string[]> {
|
|
59
|
-
const token = createJwt({ sub, exp: Math.floor(Date.now() / 1000) + 300 }, SECRET);
|
|
60
|
-
const res = await fetch(baseUrl, {
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: {
|
|
63
|
-
Authorization: `Bearer ${token}`,
|
|
64
|
-
'Content-Type': 'application/json',
|
|
65
|
-
Accept: 'application/json, text/event-stream',
|
|
66
|
-
},
|
|
67
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
|
|
68
|
-
});
|
|
69
|
-
expect(res.status).toBe(200);
|
|
70
|
-
const text = await res.text();
|
|
71
|
-
const json = text.includes('data:')
|
|
72
|
-
? JSON.parse(text.split('\n').find((l) => l.startsWith('data:'))!.slice(5))
|
|
73
|
-
: JSON.parse(text);
|
|
74
|
-
return (json.result.tools as { name: string }[]).map((t) => t.name);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
describe('tools/list over HTTP is filtered by the bearer token', () => {
|
|
78
|
-
it('admin sees admin and control tools', async () => {
|
|
79
|
-
const tools = await listTools('admin');
|
|
80
|
-
expect(tools).toContain('restartHomeAssistant');
|
|
81
|
-
expect(tools).toContain('controlLight');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('a READONLY token is not offered control or admin tools', async () => {
|
|
85
|
-
const tools = await listTools('reader');
|
|
86
|
-
expect(tools).toContain('getState');
|
|
87
|
-
expect(tools).not.toContain('controlLight');
|
|
88
|
-
expect(tools).not.toContain('restartHomeAssistant');
|
|
89
|
-
expect(tools).not.toContain('deleteAutomation');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('an unmapped sub (defaultRole NONE) is offered no permission-gated tools', async () => {
|
|
93
|
-
const tools = await listTools('nobody');
|
|
94
|
-
expect(tools).not.toContain('getState');
|
|
95
|
-
expect(tools).not.toContain('controlLight');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('interleaved callers each get their own filtered list (no shared server instance)', async () => {
|
|
99
|
-
const [reader1, admin, reader2, operator] = await Promise.all([
|
|
100
|
-
listTools('reader'),
|
|
101
|
-
listTools('admin'),
|
|
102
|
-
listTools('reader'),
|
|
103
|
-
listTools('operator'),
|
|
104
|
-
]);
|
|
105
|
-
expect(reader1).toEqual(reader2);
|
|
106
|
-
expect(reader1).not.toContain('controlLight');
|
|
107
|
-
expect(admin).toContain('restartHomeAssistant');
|
|
108
|
-
expect(operator).toContain('controlLight');
|
|
109
|
-
expect(operator).not.toContain('restartHomeAssistant');
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tools/list is filtered by the caller's permission mask.
|
|
3
|
-
*
|
|
4
|
-
* A caller is only offered tools it can actually use, so an LLM client does not
|
|
5
|
-
* plan around (or learn about) admin tools it would be denied. Execution-time
|
|
6
|
-
* RBAC in wrapToolHandler is unchanged and still enforced.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
|
|
11
|
-
import { createServer } from '../../src/server/common.js';
|
|
12
|
-
import { Role, Permission } from '../../src/permissions/index.js';
|
|
13
|
-
import { getToolRequiredPermission } from '../../src/tools/common.js';
|
|
14
|
-
import { canReadAnyResource } from '../../src/tools/omada/graph.js';
|
|
15
|
-
|
|
16
|
-
// Registration never calls the HA client; a stub is enough to register HA tools.
|
|
17
|
-
const haClient = {} as never;
|
|
18
|
-
|
|
19
|
-
async function listToolNames(callerPermissions: number | undefined): Promise<string[]> {
|
|
20
|
-
const server = createServer({ haClient, callerPermissions });
|
|
21
|
-
const client = new Client({ name: 'visibility-test', version: '0' });
|
|
22
|
-
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
23
|
-
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
24
|
-
try {
|
|
25
|
-
const { tools } = await client.listTools();
|
|
26
|
-
return tools.map((t) => t.name).sort();
|
|
27
|
-
} finally {
|
|
28
|
-
await client.close();
|
|
29
|
-
await server.close();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('tools/list visibility by caller permissions', () => {
|
|
34
|
-
it('lists every tool when no caller mask is given (stdio local trust)', async () => {
|
|
35
|
-
const all = await listToolNames(undefined);
|
|
36
|
-
expect(all).toContain('restartHomeAssistant');
|
|
37
|
-
expect(all).toContain('controlLight');
|
|
38
|
-
expect(all).toContain('getState');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('a full-permission caller sees the same list as unfiltered', async () => {
|
|
42
|
-
expect(await listToolNames(0xff)).toEqual(await listToolNames(undefined));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('a READONLY caller sees only tools it is permitted to call', async () => {
|
|
46
|
-
const all = await listToolNames(undefined);
|
|
47
|
-
const visible = await listToolNames(Role.READONLY);
|
|
48
|
-
|
|
49
|
-
expect(visible).toContain('getState');
|
|
50
|
-
expect(visible).not.toContain('controlLight');
|
|
51
|
-
expect(visible).not.toContain('restartHomeAssistant');
|
|
52
|
-
expect(visible).not.toContain('deleteAutomation');
|
|
53
|
-
expect(visible.length).toBeLessThan(all.length);
|
|
54
|
-
|
|
55
|
-
// Every hidden tool really is one the caller would be denied, and every
|
|
56
|
-
// visible tool with a static bit is one the caller holds.
|
|
57
|
-
for (const name of all) {
|
|
58
|
-
const required = getToolRequiredPermission(name);
|
|
59
|
-
const permitted = required === undefined || (Role.READONLY & required) === required;
|
|
60
|
-
expect(visible.includes(name), `${name} visibility`).toBe(permitted);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('an OPERATOR caller sees control tools but not admin/configure tools', async () => {
|
|
65
|
-
const visible = await listToolNames(Role.OPERATOR);
|
|
66
|
-
expect(visible).toContain('controlLight');
|
|
67
|
-
expect(visible).toContain('sendNotification');
|
|
68
|
-
expect(visible).not.toContain('restartHomeAssistant');
|
|
69
|
-
expect(visible).not.toContain('createAutomation');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('a caller with no permissions sees no permission-gated tools', async () => {
|
|
73
|
-
const visible = await listToolNames(Role.NONE);
|
|
74
|
-
for (const name of visible) {
|
|
75
|
-
expect(getToolRequiredPermission(name), `${name} should not be listed for NONE`).toBeUndefined();
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('a hidden tool cannot be called either', async () => {
|
|
80
|
-
const server = createServer({ haClient, callerPermissions: Permission.QUERY });
|
|
81
|
-
const client = new Client({ name: 'visibility-test', version: '0' });
|
|
82
|
-
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
83
|
-
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
84
|
-
try {
|
|
85
|
-
const outcome = await client
|
|
86
|
-
.callTool({ name: 'restartHomeAssistant', arguments: {} })
|
|
87
|
-
.then((r) => (r.isError ? 'error-result' : 'success'), () => 'rejected');
|
|
88
|
-
expect(outcome).not.toBe('success');
|
|
89
|
-
} finally {
|
|
90
|
-
await client.close();
|
|
91
|
-
await server.close();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('omada_read visibility (per-path RBAC tool)', () => {
|
|
97
|
-
// Graph mode registers omada_browse + omada_read; registration never calls the client.
|
|
98
|
-
async function listOmadaTools(callerPermissions: number): Promise<string[]> {
|
|
99
|
-
const server = createServer({ omadaClient: {} as never, toolRegistrationMode: 'graph', callerPermissions });
|
|
100
|
-
const client = new Client({ name: 'visibility-test', version: '0' });
|
|
101
|
-
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
102
|
-
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
103
|
-
try {
|
|
104
|
-
const { tools } = await client.listTools();
|
|
105
|
-
return tools.map((t) => t.name).filter((n) => n.startsWith('omada_'));
|
|
106
|
-
} finally {
|
|
107
|
-
await client.close();
|
|
108
|
-
await server.close();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
it('is hidden from a caller who can read no Omada resource (NONE)', async () => {
|
|
113
|
-
expect(await listOmadaTools(Role.NONE)).not.toContain('omada_read');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('is listed for a caller who can read some resources (QUERY)', async () => {
|
|
117
|
-
expect(await listOmadaTools(Permission.QUERY)).toContain('omada_read');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('is listed for an ADMIN-only mask, which can still read the ADMIN-gated /security nodes', async () => {
|
|
121
|
-
expect(canReadAnyResource(Permission.ADMIN)).toBe(true);
|
|
122
|
-
expect(await listOmadaTools(Permission.ADMIN)).toContain('omada_read');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('is hidden for a mask that matches no node (e.g. NOTIFY only)', async () => {
|
|
126
|
-
expect(canReadAnyResource(Permission.NOTIFY)).toBe(false);
|
|
127
|
-
expect(await listOmadaTools(Permission.NOTIFY)).not.toContain('omada_read');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for AI analyzeSensors tool handler
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
-
import type { McpServer } from '@modelcontextprotocol/server';
|
|
7
|
-
import type { LocalAIClient } from '../../../src/localAI/index.js';
|
|
8
|
-
import { registerAnalyzeSensorsTool } from '../../../src/tools/ai/analyzeSensors.js';
|
|
9
|
-
|
|
10
|
-
// Create mock server that captures registered handlers
|
|
11
|
-
function createMockServer() {
|
|
12
|
-
const handlers = new Map<string, { config: unknown; handler: (...args: unknown[]) => unknown }>();
|
|
13
|
-
return {
|
|
14
|
-
registerTool: vi.fn((name: string, config: unknown, handler: (...args: unknown[]) => unknown) => {
|
|
15
|
-
handlers.set(name, { config, handler });
|
|
16
|
-
}),
|
|
17
|
-
handlers,
|
|
18
|
-
} as unknown as McpServer & { handlers: Map<string, { config: unknown; handler: (...args: unknown[]) => unknown }> };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Create mock LocalAIClient
|
|
22
|
-
function createMockAIClient() {
|
|
23
|
-
return {
|
|
24
|
-
analyzeSensors: vi.fn(),
|
|
25
|
-
} as unknown as LocalAIClient;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Mock extra with AI permission
|
|
29
|
-
const mockExtra = {
|
|
30
|
-
sessionId: 'test-session',
|
|
31
|
-
http: { authInfo: { extra: { permissions: 0xff } } }, // All permissions including AI
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Mock extra without AI permission
|
|
35
|
-
const noAiPermExtra = {
|
|
36
|
-
sessionId: 'test-session',
|
|
37
|
-
http: { authInfo: { extra: { permissions: 0x08 } } }, // QUERY only
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Parse JSON result from tool output
|
|
41
|
-
function parseResult(result: { content: { text: string }[] }): unknown {
|
|
42
|
-
return JSON.parse(result.content[0].text);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe('AI analyzeSensors Tool Handler', () => {
|
|
46
|
-
let server: ReturnType<typeof createMockServer>;
|
|
47
|
-
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
server = createMockServer();
|
|
50
|
-
vi.clearAllMocks();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('Registration', () => {
|
|
54
|
-
it('should register the tool', () => {
|
|
55
|
-
const aiClient = createMockAIClient();
|
|
56
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
57
|
-
|
|
58
|
-
expect(server.registerTool).toHaveBeenCalledWith(
|
|
59
|
-
'analyzeSensors',
|
|
60
|
-
expect.objectContaining({
|
|
61
|
-
description: expect.any(String),
|
|
62
|
-
}),
|
|
63
|
-
expect.any(Function)
|
|
64
|
-
);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should register with optional AI client', () => {
|
|
68
|
-
registerAnalyzeSensorsTool(server, undefined);
|
|
69
|
-
|
|
70
|
-
expect(server.registerTool).toHaveBeenCalledWith(
|
|
71
|
-
'analyzeSensors',
|
|
72
|
-
expect.objectContaining({
|
|
73
|
-
description: expect.any(String),
|
|
74
|
-
}),
|
|
75
|
-
expect.any(Function)
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('AI Client Not Configured', () => {
|
|
81
|
-
it('should return error when AI client is not provided', async () => {
|
|
82
|
-
registerAnalyzeSensorsTool(server, undefined);
|
|
83
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
84
|
-
|
|
85
|
-
const sensorData = {
|
|
86
|
-
'sensor.temperature': { state: '25', unit: '°C' },
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const result = await handler({ sensors: sensorData }, mockExtra);
|
|
90
|
-
const parsed = parseResult(result) as { error: string; message: string };
|
|
91
|
-
|
|
92
|
-
expect(parsed.error).toBe('AI client not configured');
|
|
93
|
-
expect(parsed.message).toContain('AI_URL');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('Input Validation', () => {
|
|
98
|
-
it('should return error when sensors is undefined', async () => {
|
|
99
|
-
const aiClient = createMockAIClient();
|
|
100
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
101
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
102
|
-
|
|
103
|
-
const result = await handler({ sensors: undefined }, mockExtra);
|
|
104
|
-
const parsed = parseResult(result) as { error: string };
|
|
105
|
-
|
|
106
|
-
expect(parsed.error).toBe('sensors object is required');
|
|
107
|
-
expect(aiClient.analyzeSensors).not.toHaveBeenCalled();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should return error when sensors is not an object', async () => {
|
|
111
|
-
const aiClient = createMockAIClient();
|
|
112
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
113
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
114
|
-
|
|
115
|
-
const result = await handler({ sensors: 'invalid' }, mockExtra);
|
|
116
|
-
const parsed = parseResult(result) as { error: string };
|
|
117
|
-
|
|
118
|
-
expect(parsed.error).toBe('sensors object is required');
|
|
119
|
-
expect(aiClient.analyzeSensors).not.toHaveBeenCalled();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('Successful Analysis', () => {
|
|
124
|
-
it('should analyze sensors and return result', async () => {
|
|
125
|
-
const aiClient = createMockAIClient();
|
|
126
|
-
const mockAnalysis = {
|
|
127
|
-
summary: 'All sensors are operating normally',
|
|
128
|
-
issues: [],
|
|
129
|
-
recommendations: ['Consider adding humidity sensors'],
|
|
130
|
-
};
|
|
131
|
-
(aiClient.analyzeSensors as ReturnType<typeof vi.fn>).mockResolvedValue(mockAnalysis);
|
|
132
|
-
|
|
133
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
134
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
135
|
-
|
|
136
|
-
const sensorData = {
|
|
137
|
-
'sensor.temperature': { state: '25', unit: '°C', attributes: { friendly_name: 'Living Room' } },
|
|
138
|
-
'sensor.humidity': { state: '50', unit: '%', attributes: { friendly_name: 'Living Room Humidity' } },
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const result = await handler({ sensors: sensorData }, mockExtra);
|
|
142
|
-
const parsed = parseResult(result);
|
|
143
|
-
|
|
144
|
-
expect(parsed).toEqual(mockAnalysis);
|
|
145
|
-
expect(aiClient.analyzeSensors).toHaveBeenCalledWith(sensorData);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should handle empty sensor object', async () => {
|
|
149
|
-
const aiClient = createMockAIClient();
|
|
150
|
-
const mockAnalysis = { summary: 'No sensors provided for analysis', issues: [], recommendations: [] };
|
|
151
|
-
(aiClient.analyzeSensors as ReturnType<typeof vi.fn>).mockResolvedValue(mockAnalysis);
|
|
152
|
-
|
|
153
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
154
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
155
|
-
|
|
156
|
-
const result = await handler({ sensors: {} }, mockExtra);
|
|
157
|
-
const parsed = parseResult(result);
|
|
158
|
-
|
|
159
|
-
expect(parsed).toEqual(mockAnalysis);
|
|
160
|
-
expect(aiClient.analyzeSensors).toHaveBeenCalledWith({});
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('AI Client Error Handling', () => {
|
|
165
|
-
it('should handle AI client errors gracefully', async () => {
|
|
166
|
-
const aiClient = createMockAIClient();
|
|
167
|
-
(aiClient.analyzeSensors as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Ollama connection failed'));
|
|
168
|
-
|
|
169
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
170
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
171
|
-
|
|
172
|
-
const sensorData = { 'sensor.test': { state: '10' } };
|
|
173
|
-
const result = await handler({ sensors: sensorData }, mockExtra);
|
|
174
|
-
const parsed = parseResult(result) as { error: string; message: string };
|
|
175
|
-
|
|
176
|
-
expect(parsed.error).toBe('Failed to execute analyzeSensors');
|
|
177
|
-
expect(parsed.message).toContain('Ollama connection failed');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should handle timeout errors', async () => {
|
|
181
|
-
const aiClient = createMockAIClient();
|
|
182
|
-
(aiClient.analyzeSensors as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Request timeout'));
|
|
183
|
-
|
|
184
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
185
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
186
|
-
|
|
187
|
-
const sensorData = { 'sensor.test': { state: '10' } };
|
|
188
|
-
const result = await handler({ sensors: sensorData }, mockExtra);
|
|
189
|
-
const parsed = parseResult(result) as { error: string; message: string };
|
|
190
|
-
|
|
191
|
-
expect(parsed.error).toBe('Failed to execute analyzeSensors');
|
|
192
|
-
expect(parsed.message).toContain('timeout');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('Permission Enforcement', () => {
|
|
197
|
-
it('should deny access without AI permission', async () => {
|
|
198
|
-
const aiClient = createMockAIClient();
|
|
199
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
200
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
201
|
-
|
|
202
|
-
const sensorData = { 'sensor.test': { state: '10' } };
|
|
203
|
-
const result = await handler({ sensors: sensorData }, noAiPermExtra);
|
|
204
|
-
const parsed = parseResult(result) as { error: string; message: string };
|
|
205
|
-
|
|
206
|
-
expect(parsed.error).toBe('Permission denied');
|
|
207
|
-
expect(parsed.message).toContain('AI');
|
|
208
|
-
expect(aiClient.analyzeSensors).not.toHaveBeenCalled();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should allow access with AI permission', async () => {
|
|
212
|
-
const aiClient = createMockAIClient();
|
|
213
|
-
const mockAnalysis = { summary: 'Analysis complete', issues: [], recommendations: [] };
|
|
214
|
-
(aiClient.analyzeSensors as ReturnType<typeof vi.fn>).mockResolvedValue(mockAnalysis);
|
|
215
|
-
|
|
216
|
-
registerAnalyzeSensorsTool(server, aiClient);
|
|
217
|
-
const handler = server.handlers.get('analyzeSensors')!.handler;
|
|
218
|
-
|
|
219
|
-
const sensorData = { 'sensor.test': { state: '10' } };
|
|
220
|
-
const result = await handler({ sensors: sensorData }, mockExtra);
|
|
221
|
-
const parsed = parseResult(result);
|
|
222
|
-
|
|
223
|
-
expect(parsed).toEqual(mockAnalysis);
|
|
224
|
-
expect(aiClient.analyzeSensors).toHaveBeenCalled();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common tool utilities tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
-
import { wrapToolHandler, toToolResult, Permission } from '../../src/tools/common.js';
|
|
7
|
-
import type { ToolExtra } from '../../src/tools/common.js';
|
|
8
|
-
|
|
9
|
-
describe('toToolResult', () => {
|
|
10
|
-
it('should convert string to text content', () => {
|
|
11
|
-
const result = toToolResult('hello');
|
|
12
|
-
expect(result.content).toEqual([{ type: 'text', text: 'hello' }]);
|
|
13
|
-
expect(result.isError).toBe(false);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should convert object to JSON text content', () => {
|
|
17
|
-
const result = toToolResult({ foo: 'bar' });
|
|
18
|
-
expect(result.content[0].type).toBe('text');
|
|
19
|
-
expect(JSON.parse((result.content[0] as { text: string }).text)).toEqual({ foo: 'bar' });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should set isError when specified', () => {
|
|
23
|
-
const result = toToolResult('error', true);
|
|
24
|
-
expect(result.isError).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('wrapToolHandler permission checks', () => {
|
|
29
|
-
// Helper to create ToolExtra with permissions in http.authInfo.extra (MCP SDK v2 context
|
|
30
|
-
// shape). No permissions => no `http` at all, as on a request without auth info.
|
|
31
|
-
const createExtra = (permissions?: number): ToolExtra =>
|
|
32
|
-
({
|
|
33
|
-
sessionId: 'test-session',
|
|
34
|
-
mcpReq: { id: 1, signal: new AbortController().signal },
|
|
35
|
-
...(permissions !== undefined
|
|
36
|
-
? { http: { authInfo: { token: 'test-token', clientId: 'test-client', scopes: [], extra: { permissions } } } }
|
|
37
|
-
: {}),
|
|
38
|
-
}) as unknown as ToolExtra;
|
|
39
|
-
|
|
40
|
-
it('should allow access when user has required permission', async () => {
|
|
41
|
-
const handler = vi.fn().mockResolvedValue(toToolResult('success'));
|
|
42
|
-
const wrapped = wrapToolHandler('testTool', handler, Permission.QUERY);
|
|
43
|
-
|
|
44
|
-
const extra = createExtra(Permission.QUERY);
|
|
45
|
-
const result = await wrapped({}, extra);
|
|
46
|
-
|
|
47
|
-
expect(handler).toHaveBeenCalled();
|
|
48
|
-
expect(result.isError).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should deny access when user lacks required permission', async () => {
|
|
52
|
-
const handler = vi.fn().mockResolvedValue(toToolResult('success'));
|
|
53
|
-
const wrapped = wrapToolHandler('testTool', handler, Permission.ADMIN);
|
|
54
|
-
|
|
55
|
-
const extra = createExtra(Permission.QUERY); // Only QUERY, not ADMIN
|
|
56
|
-
const result = await wrapped({}, extra);
|
|
57
|
-
|
|
58
|
-
expect(handler).not.toHaveBeenCalled();
|
|
59
|
-
expect(result.isError).toBe(true);
|
|
60
|
-
const text = (result.content[0] as { text: string }).text;
|
|
61
|
-
expect(text).toContain('Permission denied');
|
|
62
|
-
expect(text).toContain('ADMIN');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should allow access with combined permissions', async () => {
|
|
66
|
-
const handler = vi.fn().mockResolvedValue(toToolResult('success'));
|
|
67
|
-
const wrapped = wrapToolHandler('testTool', handler, Permission.CONTROL);
|
|
68
|
-
|
|
69
|
-
// User has QUERY | CONTROL | NOTIFY
|
|
70
|
-
const extra = createExtra(Permission.QUERY | Permission.CONTROL | Permission.NOTIFY);
|
|
71
|
-
const result = await wrapped({}, extra);
|
|
72
|
-
|
|
73
|
-
expect(handler).toHaveBeenCalled();
|
|
74
|
-
expect(result.isError).toBe(false);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should fail closed (deny) when authInfo is not set (L3)', async () => {
|
|
78
|
-
const handler = vi.fn().mockResolvedValue(toToolResult('success'));
|
|
79
|
-
const wrapped = wrapToolHandler('testTool', handler, Permission.ADMIN);
|
|
80
|
-
|
|
81
|
-
// A missing permission mask must DENY, not default-allow. The trusted-local
|
|
82
|
-
// stdio path opts into full trust separately (see insecureDesign.test.ts).
|
|
83
|
-
const extra = createExtra(undefined);
|
|
84
|
-
const result = await wrapped({}, extra);
|
|
85
|
-
|
|
86
|
-
expect(handler).not.toHaveBeenCalled();
|
|
87
|
-
expect(result.isError).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should allow access when no permission required', async () => {
|
|
91
|
-
const handler = vi.fn().mockResolvedValue(toToolResult('success'));
|
|
92
|
-
const wrapped = wrapToolHandler('testTool', handler); // No permission required
|
|
93
|
-
|
|
94
|
-
const extra = createExtra(0); // No permissions
|
|
95
|
-
const result = await wrapped({}, extra);
|
|
96
|
-
|
|
97
|
-
expect(handler).toHaveBeenCalled();
|
|
98
|
-
expect(result.isError).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should catch and handle errors from handler', async () => {
|
|
102
|
-
const handler = vi.fn().mockRejectedValue(new Error('Something went wrong'));
|
|
103
|
-
const wrapped = wrapToolHandler('testTool', handler, Permission.QUERY);
|
|
104
|
-
|
|
105
|
-
const extra = createExtra(Permission.QUERY);
|
|
106
|
-
const result = await wrapped({}, extra);
|
|
107
|
-
|
|
108
|
-
expect(result.isError).toBe(true);
|
|
109
|
-
const text = (result.content[0] as { text: string }).text;
|
|
110
|
-
expect(text).toContain('Something went wrong');
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('Permission values', () => {
|
|
115
|
-
it('should have correct bit values sorted by criticality', () => {
|
|
116
|
-
expect(Permission.ADMIN).toBe(1); // Highest criticality
|
|
117
|
-
expect(Permission.CONFIGURE).toBe(2);
|
|
118
|
-
expect(Permission.CONTROL).toBe(4);
|
|
119
|
-
expect(Permission.QUERY).toBe(8);
|
|
120
|
-
expect(Permission.NOTIFY).toBe(16);
|
|
121
|
-
expect(Permission.AI).toBe(32);
|
|
122
|
-
});
|
|
123
|
-
});
|