@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.3.1
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 +11 -2
- 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 +6 -0
- package/dist/omadaClient/index.js +15 -0
- package/dist/omadaClient/index.js.map +1 -1
- package/dist/omadaClient/macGroups.d.ts +58 -0
- package/dist/omadaClient/macGroups.js +124 -0
- package/dist/omadaClient/macGroups.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/index.js +6 -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 +8 -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
package/src/tools/common.ts
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common tool utilities
|
|
3
|
-
* Shared helpers for all MCP tools
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { CallToolResult, ServerContext } from '@modelcontextprotocol/server';
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
import { logger } from '../utils/logger.js';
|
|
9
|
-
import { redactSecrets } from '../utils/redact.js';
|
|
10
|
-
import { Permission, hasPermission, getPermissionNames } from '../permissions/index.js';
|
|
11
|
-
|
|
12
|
-
// Re-export Permission for tool files
|
|
13
|
-
export { Permission };
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Local-trust flag for the stdio transport (L3 / OWASP A04:2021).
|
|
17
|
-
*
|
|
18
|
-
* Tool calls normally carry a permission mask in `authInfo.extra.permissions`,
|
|
19
|
-
* populated by the HTTP auth middleware (bearer mask, or 0xFF for the loopback
|
|
20
|
-
* `none` mode). When that mask is ABSENT we fail closed — deny — instead of the
|
|
21
|
-
* old default-allow `?? 0xFF`.
|
|
22
|
-
*
|
|
23
|
-
* The one legitimate no-auth path is stdio (Claude Desktop): there is no per-request
|
|
24
|
-
* auth and the process owner is the trusted local user. `startStdioServer` opts into
|
|
25
|
-
* full trust by calling `setLocalFullTrust(true)`. HTTP never enables it.
|
|
26
|
-
*/
|
|
27
|
-
let localFullTrust = false;
|
|
28
|
-
|
|
29
|
-
export function setLocalFullTrust(enabled: boolean): void {
|
|
30
|
-
localFullTrust = enabled;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Extra context passed to tool handlers
|
|
35
|
-
* Permissions are extracted from authInfo.extra.permissions (MCP SDK standard)
|
|
36
|
-
*/
|
|
37
|
-
export type ToolExtra = ServerContext;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolve the caller's permission mask from the MCP SDK auth context.
|
|
41
|
-
*
|
|
42
|
-
* L3: fail closed when the mask is absent — only the trusted-local stdio path
|
|
43
|
-
* (setLocalFullTrust) grants full permissions (0xFF) without an explicit mask.
|
|
44
|
-
* HTTP requests always carry a mask (bearer-derived, or 0xFF for loopback `none`).
|
|
45
|
-
*
|
|
46
|
-
* Exposed so tools that gate per-resource (e.g. the Omada resource-graph
|
|
47
|
-
* `omada_read`/`omada_browse`) can apply the same extraction + fail-closed
|
|
48
|
-
* semantics as `wrapToolHandler`, instead of re-reading `authInfo` ad hoc.
|
|
49
|
-
*/
|
|
50
|
-
export function getCallerPermissions(extra: ToolExtra): number {
|
|
51
|
-
return (extra.http?.authInfo?.extra?.permissions as number | undefined) ?? (localFullTrust ? 0xFF : 0);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Convert any value to a CallToolResult
|
|
56
|
-
*/
|
|
57
|
-
export function toToolResult(value: unknown, isError = false): CallToolResult {
|
|
58
|
-
// Every tool response passes here: secrets returned by Home Assistant or Omada
|
|
59
|
-
// (Wi-Fi password, RADIUS/VPN keys, access tokens) never reach the client.
|
|
60
|
-
const safe = redactSecrets(value);
|
|
61
|
-
const text = typeof safe === 'string' ? safe : JSON.stringify(safe, null, 2);
|
|
62
|
-
return {
|
|
63
|
-
content: text ? [{ type: 'text' as const, text }] : [],
|
|
64
|
-
isError,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Safely serialize a value for logging
|
|
70
|
-
*/
|
|
71
|
-
export function safeSerialize(value: unknown): string {
|
|
72
|
-
try {
|
|
73
|
-
return JSON.stringify(value);
|
|
74
|
-
} catch {
|
|
75
|
-
return '[unserializable]';
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Static permission bit each tool declares via `wrapToolHandler`, keyed by tool
|
|
81
|
-
* name. Lets the server hide tools a caller can never use from `tools/list`.
|
|
82
|
-
* Tools with no static bit (e.g. `omada_read`/`omada_browse`, which gate per
|
|
83
|
-
* resource path) are absent and therefore always listed.
|
|
84
|
-
*/
|
|
85
|
-
const toolPermissions = new Map<string, number>();
|
|
86
|
-
|
|
87
|
-
/** The static permission a tool requires, or `undefined` when it declares none. */
|
|
88
|
-
export function getToolRequiredPermission(name: string): number | undefined {
|
|
89
|
-
return toolPermissions.get(name);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Visibility rules for tools that gate per call rather than with one static bit
|
|
94
|
-
* (e.g. `omada_read`, whose permission depends on the resource path). The rule
|
|
95
|
-
* answers "can this caller use the tool at all?".
|
|
96
|
-
*/
|
|
97
|
-
const toolVisibilityRules = new Map<string, (callerPermissions: number) => boolean>();
|
|
98
|
-
|
|
99
|
-
/** Declare when a tool without a static permission should be listed to a caller. */
|
|
100
|
-
export function setToolVisibilityRule(name: string, isUsable: (callerPermissions: number) => boolean): void {
|
|
101
|
-
toolVisibilityRules.set(name, isUsable);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Whether `tools/list` should offer `name` to a caller: false when the caller lacks
|
|
106
|
-
* the tool's static permission, or when its visibility rule says the caller can use
|
|
107
|
-
* none of it. Tools with neither stay visible (their handlers still enforce RBAC).
|
|
108
|
-
*/
|
|
109
|
-
export function isToolVisibleTo(name: string, callerPermissions: number): boolean {
|
|
110
|
-
const required = toolPermissions.get(name);
|
|
111
|
-
if (required !== undefined && !hasPermission(callerPermissions, required)) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
const rule = toolVisibilityRules.get(name);
|
|
115
|
-
return rule ? rule(callerPermissions) : true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Wrap a tool handler with logging, error handling, and permission checking
|
|
120
|
-
* Includes session ID and args in logs for debugging
|
|
121
|
-
*
|
|
122
|
-
* @param name - Tool name for logging
|
|
123
|
-
* @param handler - The actual tool handler function
|
|
124
|
-
* @param requiredPermission - Optional permission mask required to use this tool
|
|
125
|
-
*/
|
|
126
|
-
export function wrapToolHandler<T>(
|
|
127
|
-
name: string,
|
|
128
|
-
handler: (args: T, extra: ToolExtra) => Promise<CallToolResult>,
|
|
129
|
-
requiredPermission?: number
|
|
130
|
-
): (args: T, extra: ToolExtra) => Promise<CallToolResult> {
|
|
131
|
-
if (requiredPermission !== undefined) {
|
|
132
|
-
toolPermissions.set(name, requiredPermission);
|
|
133
|
-
}
|
|
134
|
-
return async (args: T, extra: ToolExtra): Promise<CallToolResult> => {
|
|
135
|
-
const sessionId = extra.sessionId ?? 'unknown-session';
|
|
136
|
-
// Extract permissions from MCP SDK authInfo.extra.permissions.
|
|
137
|
-
// L3: fail closed when the mask is absent — only the trusted-local stdio path
|
|
138
|
-
// (setLocalFullTrust) grants full permissions without an explicit mask.
|
|
139
|
-
const userPermissions = getCallerPermissions(extra);
|
|
140
|
-
|
|
141
|
-
// Check permissions if required
|
|
142
|
-
if (requiredPermission !== undefined && !hasPermission(userPermissions, requiredPermission)) {
|
|
143
|
-
const required = getPermissionNames(requiredPermission);
|
|
144
|
-
const has = getPermissionNames(userPermissions);
|
|
145
|
-
logger.warn('Permission denied', { tool: name, sessionId, required, has });
|
|
146
|
-
return toToolResult({
|
|
147
|
-
error: 'Permission denied',
|
|
148
|
-
message: `Tool '${name}' requires permission: ${required.join(', ')}`,
|
|
149
|
-
required,
|
|
150
|
-
has,
|
|
151
|
-
}, true);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Arguments can carry message text, entity names or MAC addresses: they are
|
|
155
|
-
// logged at debug level only, never in normal (info) operation.
|
|
156
|
-
logger.info('Tool invoked', { tool: name, sessionId });
|
|
157
|
-
logger.debug('Tool arguments', { tool: name, sessionId, args: safeSerialize(args) });
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const result = await handler(args, extra);
|
|
161
|
-
logger.info('Tool completed', { tool: name, sessionId });
|
|
162
|
-
return result;
|
|
163
|
-
} catch (error) {
|
|
164
|
-
logger.error('Tool failed', {
|
|
165
|
-
tool: name,
|
|
166
|
-
sessionId,
|
|
167
|
-
error: error instanceof Error ? error.message : String(error),
|
|
168
|
-
});
|
|
169
|
-
return toToolResult({
|
|
170
|
-
error: `Failed to execute ${name}`,
|
|
171
|
-
message: error instanceof Error ? error.message : String(error),
|
|
172
|
-
}, true);
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Common Zod schemas for tool inputs
|
|
179
|
-
*/
|
|
180
|
-
|
|
181
|
-
// Empty schema for tools with no inputs
|
|
182
|
-
export const emptySchema = z.object({});
|
|
183
|
-
|
|
184
|
-
// Entity ID schema
|
|
185
|
-
export const entityIdSchema = z.object({
|
|
186
|
-
entity_id: z.string().describe('The entity ID (e.g., "light.living_room", "sensor.temperature")'),
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Domain schema
|
|
190
|
-
export const domainSchema = z.object({
|
|
191
|
-
domain: z.string().describe('Domain name (e.g., "light", "sensor", "switch", "climate")'),
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Search query schema
|
|
195
|
-
export const searchSchema = z.object({
|
|
196
|
-
query: z.string().describe('Search query (searches in entity_id and friendly_name)'),
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// History schema
|
|
200
|
-
export const historySchema = z.object({
|
|
201
|
-
entity_id: z.string().describe('The entity ID to get history for'),
|
|
202
|
-
hours: z.number().optional().describe('Number of hours of history to retrieve (default: 24)'),
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Entity action schema
|
|
206
|
-
export const entityActionSchema = z.object({
|
|
207
|
-
entity_id: z.string().describe('The entity ID to perform the action on'),
|
|
208
|
-
action: z.enum(['turn_on', 'turn_off', 'toggle']).describe('The action to perform'),
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// List entities filter schema
|
|
212
|
-
export const listEntitiesSchema = z.object({
|
|
213
|
-
domain: z.string().optional().describe('Filter by domain (e.g., "light", "switch", "sensor")'),
|
|
214
|
-
search: z.string().optional().describe('Search query to filter by entity_id or friendly_name'),
|
|
215
|
-
state: z.string().optional().describe('Filter by state (e.g., "on", "off", "unavailable")'),
|
|
216
|
-
limit: z.number().optional().describe('Maximum number of entities to return'),
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Automation state filter schema
|
|
220
|
-
export const automationFilterSchema = z.object({
|
|
221
|
-
state: z.enum(['on', 'off']).optional().describe('Filter by automation state (enabled/disabled)'),
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// System log schema
|
|
225
|
-
export const systemLogSchema = z.object({
|
|
226
|
-
hours: z.number().optional().describe('Number of hours of history to retrieve (default: 24, max: 168)'),
|
|
227
|
-
entity_id: z.string().optional().describe('Optional entity ID to filter logs for a specific entity'),
|
|
228
|
-
limit: z.number().optional().describe('Maximum number of entries to return (default: 100)'),
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// Automation ID schema
|
|
232
|
-
export const automationIdSchema = z.object({
|
|
233
|
-
automation_id: z.string().describe('The unique ID of the automation'),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// Service call schema
|
|
237
|
-
export const serviceCallSchema = z.object({
|
|
238
|
-
domain: z.string().describe('Service domain (e.g., "light", "switch", "climate", "notify")'),
|
|
239
|
-
service: z.string().describe('Service name (e.g., "turn_on", "turn_off", "set_temperature")'),
|
|
240
|
-
service_data: z.record(z.string(), z.unknown()).optional().describe('Optional service data'),
|
|
241
|
-
target: z.object({
|
|
242
|
-
entity_id: z.union([z.string(), z.array(z.string())]).optional(),
|
|
243
|
-
device_id: z.union([z.string(), z.array(z.string())]).optional(),
|
|
244
|
-
area_id: z.union([z.string(), z.array(z.string())]).optional(),
|
|
245
|
-
}).optional().describe('Optional target'),
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Analyze sensors schema
|
|
249
|
-
export const analyzeSensorsSchema = z.object({
|
|
250
|
-
sensors: z.record(z.string(), z.unknown()).describe('Sensor data to analyze. Object with entity_id keys and {state, unit, attributes} values.'),
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// Trigger automation schema
|
|
254
|
-
export const triggerAutomationSchema = z.object({
|
|
255
|
-
entity_id: z.string().describe('The automation entity ID (e.g., "automation.turn_on_lights")'),
|
|
256
|
-
variables: z.record(z.string(), z.unknown()).optional().describe('Optional variables to pass to the automation'),
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// Create automation schema
|
|
260
|
-
export const createAutomationSchema = z.object({
|
|
261
|
-
alias: z.string().describe('Friendly name for the automation'),
|
|
262
|
-
description: z.string().optional().describe('Optional description of what the automation does'),
|
|
263
|
-
mode: z.enum(['single', 'restart', 'queued', 'parallel']).optional().describe('Execution mode (default: single)'),
|
|
264
|
-
trigger: z.unknown().describe('Trigger(s) that start the automation. Each trigger needs a "platform" property.'),
|
|
265
|
-
condition: z.unknown().optional().describe('Optional condition(s) that must be true for actions to run.'),
|
|
266
|
-
action: z.unknown().describe('Action(s) to perform when triggered. Each action typically has "service" and "target" properties.'),
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Control light schema
|
|
270
|
-
export const controlLightSchema = z.object({
|
|
271
|
-
entity_id: z.string().describe('The light entity ID (e.g., "light.living_room")'),
|
|
272
|
-
action: z.enum(['turn_on', 'turn_off', 'toggle']).describe('The action to perform'),
|
|
273
|
-
brightness_pct: z.number().optional().describe('Brightness percentage (0-100)'),
|
|
274
|
-
color_temp_kelvin: z.number().optional().describe('Color temperature in Kelvin (e.g., 2700 for warm, 6500 for cool)'),
|
|
275
|
-
rgb_color: z.tuple([z.number(), z.number(), z.number()]).optional().describe('RGB color as [red, green, blue] (0-255 each)'),
|
|
276
|
-
transition: z.number().optional().describe('Transition time in seconds'),
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Control climate schema
|
|
280
|
-
export const controlClimateSchema = z.object({
|
|
281
|
-
entity_id: z.string().describe('The climate entity ID (e.g., "climate.living_room")'),
|
|
282
|
-
temperature: z.number().optional().describe('Target temperature'),
|
|
283
|
-
hvac_mode: z.enum(['off', 'heat', 'cool', 'heat_cool', 'auto', 'dry', 'fan_only']).optional().describe('HVAC mode'),
|
|
284
|
-
fan_mode: z.string().optional().describe('Fan mode (device-specific)'),
|
|
285
|
-
preset_mode: z.string().optional().describe('Preset mode (e.g., "home", "away", "eco")'),
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Control media player schema
|
|
289
|
-
export const controlMediaPlayerSchema = z.object({
|
|
290
|
-
entity_id: z.string().describe('The media player entity ID'),
|
|
291
|
-
action: z.enum(['play', 'pause', 'stop', 'next', 'previous', 'volume_set', 'volume_mute']).describe('The action to perform'),
|
|
292
|
-
volume_level: z.number().optional().describe('Volume level (0.0 to 1.0)'),
|
|
293
|
-
is_volume_muted: z.boolean().optional().describe('Mute state'),
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// Control cover schema
|
|
297
|
-
export const controlCoverSchema = z.object({
|
|
298
|
-
entity_id: z.string().describe('The cover entity ID (e.g., "cover.living_room_blinds")'),
|
|
299
|
-
action: z.enum(['open', 'close', 'stop', 'set_position', 'set_tilt']).describe('The action to perform'),
|
|
300
|
-
position: z.number().optional().describe('Cover position (0 = closed, 100 = open)'),
|
|
301
|
-
tilt_position: z.number().optional().describe('Tilt position (0-100)'),
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// Control fan schema
|
|
305
|
-
export const controlFanSchema = z.object({
|
|
306
|
-
entity_id: z.string().describe('The fan entity ID'),
|
|
307
|
-
action: z.enum(['turn_on', 'turn_off', 'toggle', 'set_speed', 'oscillate', 'set_direction']).describe('The action to perform'),
|
|
308
|
-
percentage: z.number().optional().describe('Speed percentage (0-100)'),
|
|
309
|
-
oscillating: z.boolean().optional().describe('Oscillation state'),
|
|
310
|
-
direction: z.enum(['forward', 'reverse']).optional().describe('Fan direction'),
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Run script schema
|
|
314
|
-
export const runScriptSchema = z.object({
|
|
315
|
-
entity_id: z.string().describe('The script entity ID (e.g., "script.morning_routine")'),
|
|
316
|
-
variables: z.record(z.string(), z.unknown()).optional().describe('Optional variables to pass to the script'),
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Notification action schema for actionable notifications
|
|
320
|
-
export const notificationActionSchema = z.object({
|
|
321
|
-
action: z.string().describe('Action identifier returned when tapped'),
|
|
322
|
-
title: z.string().describe('Button text shown to user'),
|
|
323
|
-
uri: z.string().optional().describe('URI to open when tapped (e.g., "https://example.com" or "app://settings")'),
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// Send notification schema with mobile app support
|
|
327
|
-
export const sendNotificationSchema = z.object({
|
|
328
|
-
message: z.string().describe('The notification message. Use "clear_notification" to clear a notification by tag.'),
|
|
329
|
-
title: z.string().optional().describe('Optional notification title'),
|
|
330
|
-
target: z.string().optional().describe('Notification service target (e.g., "mobile_app_iphone", "mobile_app_pixel"). Defaults to "persistent_notification".'),
|
|
331
|
-
// Mobile app specific options
|
|
332
|
-
tag: z.string().optional().describe('Notification tag for grouping/replacing. Use same tag to update existing notification.'),
|
|
333
|
-
group: z.string().optional().describe('Group notifications together (Android: group key, iOS: thread-id)'),
|
|
334
|
-
priority: z.enum(['high', 'normal', 'low', 'min']).optional().describe('Notification priority (Android). "high" wakes device.'),
|
|
335
|
-
importance: z.enum(['high', 'default', 'low', 'min']).optional().describe('Notification importance (Android channel importance)'),
|
|
336
|
-
ttl: z.number().optional().describe('Time to live in seconds. Notification expires after this time.'),
|
|
337
|
-
// Actions
|
|
338
|
-
actions: z.array(notificationActionSchema).max(3).optional().describe('Up to 3 action buttons for actionable notifications'),
|
|
339
|
-
// Media
|
|
340
|
-
image: z.string().optional().describe('URL to image to display in notification'),
|
|
341
|
-
video: z.string().optional().describe('URL to video (iOS only)'),
|
|
342
|
-
icon_url: z.string().optional().describe('URL to custom notification icon (Android only)'),
|
|
343
|
-
// Behavior
|
|
344
|
-
sticky: z.boolean().optional().describe('If true, notification cannot be dismissed by swiping (Android)'),
|
|
345
|
-
persistent: z.boolean().optional().describe('If true, notification persists across reboots (Android)'),
|
|
346
|
-
clickAction: z.string().optional().describe('URI to open when notification is tapped'),
|
|
347
|
-
// Sound
|
|
348
|
-
channel: z.string().optional().describe('Android notification channel ID'),
|
|
349
|
-
vibrationPattern: z.array(z.number()).optional().describe('Custom vibration pattern (Android) e.g., [100, 200, 100]'),
|
|
350
|
-
ledColor: z.string().optional().describe('LED color as hex (Android) e.g., "#FF0000"'),
|
|
351
|
-
// iOS specific
|
|
352
|
-
sound: z.string().optional().describe('Sound file name (iOS) or "default"'),
|
|
353
|
-
badge: z.number().optional().describe('App badge count (iOS)'),
|
|
354
|
-
interruptionLevel: z.enum(['passive', 'active', 'time-sensitive', 'critical']).optional().describe('iOS interruption level'),
|
|
355
|
-
// Raw data passthrough for advanced use
|
|
356
|
-
data: z.record(z.string(), z.unknown()).optional().describe('Additional raw data to pass to notification service'),
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// List notification targets schema (empty - no params needed)
|
|
360
|
-
export const listNotificationTargetsSchema = z.object({});
|
|
361
|
-
|
|
362
|
-
// List calendars schema (empty - no params needed)
|
|
363
|
-
export const listCalendarsSchema = z.object({});
|
|
364
|
-
|
|
365
|
-
// Get calendar events schema
|
|
366
|
-
export const getCalendarEventsSchema = z.object({
|
|
367
|
-
entity_id: z.string().optional().describe('Calendar entity ID (e.g., "calendar.my_calendar"). If not provided, gets events from all calendars.'),
|
|
368
|
-
start_date: z.string().optional().describe('Start date in ISO format (e.g., "2026-01-01"). Defaults to today.'),
|
|
369
|
-
end_date: z.string().optional().describe('End date in ISO format (e.g., "2026-12-31"). Defaults to 30 days from start.'),
|
|
370
|
-
days: z.number().optional().describe('Alternative: number of days from start_date (overrides end_date if provided)'),
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// =============================================================================
|
|
374
|
-
// Omada-specific schemas
|
|
375
|
-
// =============================================================================
|
|
376
|
-
|
|
377
|
-
// Site ID input schema (optional site selector)
|
|
378
|
-
export const siteInputSchema = z.object({
|
|
379
|
-
siteId: z.string().min(1).optional().describe('Site ID (optional, uses default if not provided)'),
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Client ID schema (extends site input)
|
|
383
|
-
export const clientIdSchema = siteInputSchema.extend({
|
|
384
|
-
clientId: z.string().min(1, 'clientId (MAC or client identifier) is required').describe('Client MAC address or identifier'),
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// Device ID schema (extends site input)
|
|
388
|
-
export const deviceIdSchema = siteInputSchema.extend({
|
|
389
|
-
deviceId: z.string().min(1, 'deviceId (MAC or device identifier) is required').describe('Device MAC address or identifier'),
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// Stack ID schema (extends site input)
|
|
393
|
-
export const stackIdSchema = siteInputSchema.extend({
|
|
394
|
-
stackId: z.string().min(1, 'stackId is required').describe('Switch stack identifier'),
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// =============================================================================
|
|
398
|
-
// Home Assistant pagination schemas
|
|
399
|
-
// =============================================================================
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Pagination schema for Home Assistant entity queries.
|
|
403
|
-
* Reduces response size by returning paginated, lightweight entities by default.
|
|
404
|
-
*/
|
|
405
|
-
export const haPaginationSchema = z.object({
|
|
406
|
-
page: z.number().int().min(1).default(1).describe('Page number (starts at 1)'),
|
|
407
|
-
pageSize: z.number().int().min(1).max(200).default(50).describe('Entities per page (1-200, default 50)'),
|
|
408
|
-
includeAttributes: z.boolean().default(false).describe('Include full attributes (increases response size significantly)'),
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Pagination parameters interface for Home Assistant queries.
|
|
413
|
-
*/
|
|
414
|
-
export interface HAPaginationParams {
|
|
415
|
-
page: number;
|
|
416
|
-
pageSize: number;
|
|
417
|
-
includeAttributes: boolean;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Apply pagination to an array and return paginated result metadata.
|
|
422
|
-
*/
|
|
423
|
-
export function paginateArray<T>(items: T[], page: number, pageSize: number): {
|
|
424
|
-
page: number;
|
|
425
|
-
pageSize: number;
|
|
426
|
-
totalCount: number;
|
|
427
|
-
totalPages: number;
|
|
428
|
-
items: T[];
|
|
429
|
-
} {
|
|
430
|
-
const totalCount = items.length;
|
|
431
|
-
const totalPages = Math.ceil(totalCount / pageSize);
|
|
432
|
-
const start = (page - 1) * pageSize;
|
|
433
|
-
const paginatedItems = items.slice(start, start + pageSize);
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
page,
|
|
437
|
-
pageSize,
|
|
438
|
-
totalCount,
|
|
439
|
-
totalPages,
|
|
440
|
-
items: paginatedItems,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Owner question tools: askOwner / getOwnerAnswer / cancelOwnerQuestion.
|
|
3
|
-
* See src/ownerQuestions/service.ts for the design.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { McpServer } from '@modelcontextprotocol/server';
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
|
|
9
|
-
import { LIMITS, type OwnerQuestionService } from '../../ownerQuestions/service.js';
|
|
10
|
-
import { validatePlainText } from '../../utils/plainText.js';
|
|
11
|
-
import { toToolResult, wrapToolHandler, Permission, type ToolExtra } from '../common.js';
|
|
12
|
-
|
|
13
|
-
const plainText = (field: string, maxLength: number, allowNewlines = false) =>
|
|
14
|
-
z.string().transform((value, ctx) => {
|
|
15
|
-
try {
|
|
16
|
-
return validatePlainText(value, { field, maxLength, allowNewlines });
|
|
17
|
-
} catch (error) {
|
|
18
|
-
ctx.addIssue({ code: 'custom', message: error instanceof Error ? error.message : String(error) });
|
|
19
|
-
return z.NEVER;
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export const askOwnerSchema = z.object({
|
|
24
|
-
question: plainText('question', LIMITS.questionChars, true).describe(
|
|
25
|
-
`The question, plain text only (no emoji), at most ${LIMITS.questionChars} characters; longer text is refused, not cut`
|
|
26
|
-
),
|
|
27
|
-
title: plainText('title', LIMITS.titleChars).optional().describe(`Notification title, at most ${LIMITS.titleChars} characters (default "Approval needed")`),
|
|
28
|
-
buttons: z
|
|
29
|
-
.array(plainText('button label', LIMITS.buttonChars))
|
|
30
|
-
.min(1)
|
|
31
|
-
.max(LIMITS.maxButtons)
|
|
32
|
-
.refine((labels) => new Set(labels.map((l) => l.toLowerCase())).size === labels.length, 'Button labels must be unique')
|
|
33
|
-
.optional()
|
|
34
|
-
.describe('1-3 button labels; default ["Approve", "Deny"]. The answer reports the tapped index (0 = first).'),
|
|
35
|
-
target: z
|
|
36
|
-
.string()
|
|
37
|
-
.regex(/^mobile_app_[a-z0-9_]{1,64}$/, 'target must be a mobile app notify service, e.g. "mobile_app_pixel_7"')
|
|
38
|
-
.describe('The phone to ask: its notify service name, e.g. "mobile_app_pixel_7"'),
|
|
39
|
-
timeoutSeconds: z
|
|
40
|
-
.number()
|
|
41
|
-
.int()
|
|
42
|
-
.min(LIMITS.minTimeoutSeconds)
|
|
43
|
-
.max(LIMITS.maxTimeoutSeconds)
|
|
44
|
-
.optional()
|
|
45
|
-
.describe(`How long the question stays open (default ${LIMITS.defaultTimeoutSeconds} s, max 7 days). No answer by then = timeout, treat as NO.`),
|
|
46
|
-
channel: plainText('channel', 40).optional().describe('Android notification channel name'),
|
|
47
|
-
importance: z.enum(['high', 'default', 'low', 'min']).optional().describe('Android notification channel importance'),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const requestIdSchema = z.string().min(1).max(4096).describe('The requestId returned by askOwner');
|
|
51
|
-
|
|
52
|
-
export const getOwnerAnswerSchema = z.object({
|
|
53
|
-
requestId: requestIdSchema,
|
|
54
|
-
waitSeconds: z
|
|
55
|
-
.number()
|
|
56
|
-
.int()
|
|
57
|
-
.min(0)
|
|
58
|
-
.max(LIMITS.maxWaitSeconds)
|
|
59
|
-
.optional()
|
|
60
|
-
.describe(`Wait up to this many seconds for an answer before returning "pending" (default 25, max ${LIMITS.maxWaitSeconds})`),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
export const cancelOwnerQuestionSchema = z.object({ requestId: requestIdSchema });
|
|
64
|
-
|
|
65
|
-
/** The token `sub` of the caller; stdio (local trust) has none. */
|
|
66
|
-
function callerOf(extra: ToolExtra): string {
|
|
67
|
-
return extra.http?.authInfo?.clientId ?? 'local';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function registerOwnerQuestionTools(server: McpServer, service: OwnerQuestionService): number {
|
|
71
|
-
server.registerTool(
|
|
72
|
-
'askOwner',
|
|
73
|
-
{
|
|
74
|
-
description:
|
|
75
|
-
"Ask the owner a question on their phone with up to 3 buttons (default Approve / Deny) and return immediately with a requestId. " +
|
|
76
|
-
'Then call getOwnerAnswer(requestId) until the status is not "pending". ' +
|
|
77
|
-
'Anything except an explicit approving answer must be treated as NO: deny, timeout, cancelled and unknown. ' +
|
|
78
|
-
'The owner may be away from the phone for hours; use a timeout that fits.',
|
|
79
|
-
inputSchema: askOwnerSchema,
|
|
80
|
-
},
|
|
81
|
-
wrapToolHandler(
|
|
82
|
-
'askOwner',
|
|
83
|
-
async (args: z.infer<typeof askOwnerSchema>, extra: ToolExtra) =>
|
|
84
|
-
toToolResult(
|
|
85
|
-
await service.ask(
|
|
86
|
-
{
|
|
87
|
-
question: args.question,
|
|
88
|
-
title: args.title ?? 'Approval needed',
|
|
89
|
-
buttons: args.buttons ?? ['Approve', 'Deny'],
|
|
90
|
-
target: args.target,
|
|
91
|
-
timeoutSeconds: args.timeoutSeconds ?? LIMITS.defaultTimeoutSeconds,
|
|
92
|
-
channel: args.channel,
|
|
93
|
-
importance: args.importance,
|
|
94
|
-
},
|
|
95
|
-
callerOf(extra)
|
|
96
|
-
)
|
|
97
|
-
),
|
|
98
|
-
Permission.NOTIFY
|
|
99
|
-
)
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
server.registerTool(
|
|
103
|
-
'getOwnerAnswer',
|
|
104
|
-
{
|
|
105
|
-
description:
|
|
106
|
-
'Get the answer to an askOwner question, waiting up to waitSeconds. Status: "answered" (with index, button, answeredAt, answeredBy), ' +
|
|
107
|
-
'"pending" (ask again), "timeout", "cancelled" or "unknown". Only the client that asked can read the answer.',
|
|
108
|
-
inputSchema: getOwnerAnswerSchema,
|
|
109
|
-
},
|
|
110
|
-
wrapToolHandler(
|
|
111
|
-
'getOwnerAnswer',
|
|
112
|
-
async (args: z.infer<typeof getOwnerAnswerSchema>, extra: ToolExtra) =>
|
|
113
|
-
toToolResult(await service.getAnswer(args.requestId, callerOf(extra), args.waitSeconds ?? 25)),
|
|
114
|
-
Permission.NOTIFY
|
|
115
|
-
)
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
server.registerTool(
|
|
119
|
-
'cancelOwnerQuestion',
|
|
120
|
-
{
|
|
121
|
-
description: 'Withdraw an open askOwner question; the phone notification changes to "Cancelled". Returns the final status.',
|
|
122
|
-
inputSchema: cancelOwnerQuestionSchema,
|
|
123
|
-
},
|
|
124
|
-
wrapToolHandler(
|
|
125
|
-
'cancelOwnerQuestion',
|
|
126
|
-
async (args: z.infer<typeof cancelOwnerQuestionSchema>, extra: ToolExtra) =>
|
|
127
|
-
toToolResult(await service.cancel(args.requestId, callerOf(extra))),
|
|
128
|
-
Permission.NOTIFY
|
|
129
|
-
)
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
return 3;
|
|
133
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Automation control tools - Enable, disable, toggle, reload automations
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { McpServer } from '@modelcontextprotocol/server';
|
|
6
|
-
import type { HaClient } from '../../haClient/index.js';
|
|
7
|
-
import { entityIdSchema, emptySchema, toToolResult, wrapToolHandler, Permission } from '../common.js';
|
|
8
|
-
import type { z } from 'zod';
|
|
9
|
-
|
|
10
|
-
type EntityIdArgs = z.infer<typeof entityIdSchema>;
|
|
11
|
-
|
|
12
|
-
export function registerEnableAutomationTool(server: McpServer, client: HaClient): void {
|
|
13
|
-
server.registerTool(
|
|
14
|
-
'enableAutomation',
|
|
15
|
-
{
|
|
16
|
-
description: 'Enable a disabled Home Assistant automation.',
|
|
17
|
-
inputSchema: entityIdSchema,
|
|
18
|
-
},
|
|
19
|
-
wrapToolHandler('enableAutomation', async ({ entity_id }: EntityIdArgs) => {
|
|
20
|
-
const result = await client.enableAutomation(entity_id);
|
|
21
|
-
return toToolResult({ success: true, entity_id, action: 'enabled', context: result.context });
|
|
22
|
-
}, Permission.CONFIGURE)
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function registerDisableAutomationTool(server: McpServer, client: HaClient): void {
|
|
27
|
-
server.registerTool(
|
|
28
|
-
'disableAutomation',
|
|
29
|
-
{
|
|
30
|
-
description: 'Disable a Home Assistant automation.',
|
|
31
|
-
inputSchema: entityIdSchema,
|
|
32
|
-
},
|
|
33
|
-
wrapToolHandler('disableAutomation', async ({ entity_id }: EntityIdArgs) => {
|
|
34
|
-
const result = await client.disableAutomation(entity_id);
|
|
35
|
-
return toToolResult({ success: true, entity_id, action: 'disabled', context: result.context });
|
|
36
|
-
}, Permission.CONFIGURE)
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function registerToggleAutomationTool(server: McpServer, client: HaClient): void {
|
|
41
|
-
server.registerTool(
|
|
42
|
-
'toggleAutomation',
|
|
43
|
-
{
|
|
44
|
-
description: 'Toggle a Home Assistant automation (enable if disabled, disable if enabled).',
|
|
45
|
-
inputSchema: entityIdSchema,
|
|
46
|
-
},
|
|
47
|
-
wrapToolHandler('toggleAutomation', async ({ entity_id }: EntityIdArgs) => {
|
|
48
|
-
const result = await client.toggleAutomation(entity_id);
|
|
49
|
-
return toToolResult({ success: true, entity_id, action: 'toggled', context: result.context });
|
|
50
|
-
}, Permission.CONFIGURE)
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function registerReloadAutomationsTool(server: McpServer, client: HaClient): void {
|
|
55
|
-
server.registerTool(
|
|
56
|
-
'reloadAutomations',
|
|
57
|
-
{
|
|
58
|
-
description: 'Reload all automations from the Home Assistant configuration. Useful after editing automation YAML files.',
|
|
59
|
-
inputSchema: emptySchema,
|
|
60
|
-
},
|
|
61
|
-
wrapToolHandler('reloadAutomations', async () => {
|
|
62
|
-
await client.reloadAutomations();
|
|
63
|
-
return toToolResult({ success: true, message: 'Automations reloaded from configuration' });
|
|
64
|
-
}, Permission.CONFIGURE)
|
|
65
|
-
);
|
|
66
|
-
}
|