#
tokens: 17991/50000 20/20 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── bun.lock
├── func-calling
│   ├── .env.example
│   ├── .gitignore
│   ├── bun.lock
│   ├── package.json
│   ├── src
│   │   ├── data-manager
│   │   │   ├── config.ts
│   │   │   ├── data-manager.ts
│   │   │   └── data.ts
│   │   ├── hass-ws-client
│   │   │   └── client.ts
│   │   └── index.ts
│   └── tsconfig.json
├── mcp-server
│   ├── .env.example
│   ├── .gitignore
│   ├── bun.lock
│   ├── package.json
│   ├── src
│   │   ├── data-manager
│   │   │   ├── config.ts
│   │   │   ├── data-manager.ts
│   │   │   └── data.ts
│   │   ├── hass-ws-client
│   │   │   └── client.ts
│   │   └── index.ts
│   └── tsconfig.json
└── README.md
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | .env
2 | dist/
3 | node_modules/
4 | 
```

--------------------------------------------------------------------------------
/mcp-server/.env.example:
--------------------------------------------------------------------------------

```
1 | HOME_ASSISTANT_TOKEN="<your-home-assistant-token>"
2 | HOME_ASSISTANT_HOST="homeassistant.local:8123"
3 | HOME_ASSISTANT_SECURE="false"
4 | 
```

--------------------------------------------------------------------------------
/func-calling/.env.example:
--------------------------------------------------------------------------------

```
1 | OPEN_AI_API_KEY="your-openai-api-key"
2 | 
3 | HOME_ASSISTANT_TOKEN="<your-home-assistant-token>"
4 | HOME_ASSISTANT_HOST="homeassistant.local:8123"
5 | HOME_ASSISTANT_SECURE="false"
6 | 
```

--------------------------------------------------------------------------------
/func-calling/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
  2 | 
  3 | # Logs
  4 | 
  5 | logs
  6 | _.log
  7 | npm-debug.log_
  8 | yarn-debug.log*
  9 | yarn-error.log*
 10 | lerna-debug.log*
 11 | .pnpm-debug.log*
 12 | 
 13 | # Caches
 14 | 
 15 | .cache
 16 | 
 17 | # Diagnostic reports (https://nodejs.org/api/report.html)
 18 | 
 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
 20 | 
 21 | # Runtime data
 22 | 
 23 | pids
 24 | _.pid
 25 | _.seed
 26 | *.pid.lock
 27 | 
 28 | # Directory for instrumented libs generated by jscoverage/JSCover
 29 | 
 30 | lib-cov
 31 | 
 32 | # Coverage directory used by tools like istanbul
 33 | 
 34 | coverage
 35 | *.lcov
 36 | 
 37 | # nyc test coverage
 38 | 
 39 | .nyc_output
 40 | 
 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 42 | 
 43 | .grunt
 44 | 
 45 | # Bower dependency directory (https://bower.io/)
 46 | 
 47 | bower_components
 48 | 
 49 | # node-waf configuration
 50 | 
 51 | .lock-wscript
 52 | 
 53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 54 | 
 55 | build/Release
 56 | 
 57 | # Dependency directories
 58 | 
 59 | node_modules/
 60 | jspm_packages/
 61 | 
 62 | # Snowpack dependency directory (https://snowpack.dev/)
 63 | 
 64 | web_modules/
 65 | 
 66 | # TypeScript cache
 67 | 
 68 | *.tsbuildinfo
 69 | 
 70 | # Optional npm cache directory
 71 | 
 72 | .npm
 73 | 
 74 | # Optional eslint cache
 75 | 
 76 | .eslintcache
 77 | 
 78 | # Optional stylelint cache
 79 | 
 80 | .stylelintcache
 81 | 
 82 | # Microbundle cache
 83 | 
 84 | .rpt2_cache/
 85 | .rts2_cache_cjs/
 86 | .rts2_cache_es/
 87 | .rts2_cache_umd/
 88 | 
 89 | # Optional REPL history
 90 | 
 91 | .node_repl_history
 92 | 
 93 | # Output of 'npm pack'
 94 | 
 95 | *.tgz
 96 | 
 97 | # Yarn Integrity file
 98 | 
 99 | .yarn-integrity
100 | 
101 | # dotenv environment variable files
102 | 
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 | 
109 | # parcel-bundler cache (https://parceljs.org/)
110 | 
111 | .parcel-cache
112 | 
113 | # Next.js build output
114 | 
115 | .next
116 | out
117 | 
118 | # Nuxt.js build / generate output
119 | 
120 | .nuxt
121 | dist
122 | 
123 | # Gatsby files
124 | 
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 | 
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 | 
129 | # public
130 | 
131 | # vuepress build output
132 | 
133 | .vuepress/dist
134 | 
135 | # vuepress v2.x temp and cache directory
136 | 
137 | .temp
138 | 
139 | # Docusaurus cache and generated files
140 | 
141 | .docusaurus
142 | 
143 | # Serverless directories
144 | 
145 | .serverless/
146 | 
147 | # FuseBox cache
148 | 
149 | .fusebox/
150 | 
151 | # DynamoDB Local files
152 | 
153 | .dynamodb/
154 | 
155 | # TernJS port file
156 | 
157 | .tern-port
158 | 
159 | # Stores VSCode versions used for testing VSCode extensions
160 | 
161 | .vscode-test
162 | 
163 | # yarn v2
164 | 
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 | 
171 | # IntelliJ based IDEs
172 | .idea
173 | 
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 | 
```

--------------------------------------------------------------------------------
/mcp-server/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
  2 | 
  3 | # Logs
  4 | 
  5 | logs
  6 | _.log
  7 | npm-debug.log_
  8 | yarn-debug.log*
  9 | yarn-error.log*
 10 | lerna-debug.log*
 11 | .pnpm-debug.log*
 12 | 
 13 | # Caches
 14 | 
 15 | .cache
 16 | 
 17 | # Diagnostic reports (https://nodejs.org/api/report.html)
 18 | 
 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
 20 | 
 21 | # Runtime data
 22 | 
 23 | pids
 24 | _.pid
 25 | _.seed
 26 | *.pid.lock
 27 | 
 28 | # Directory for instrumented libs generated by jscoverage/JSCover
 29 | 
 30 | lib-cov
 31 | 
 32 | # Coverage directory used by tools like istanbul
 33 | 
 34 | coverage
 35 | *.lcov
 36 | 
 37 | # nyc test coverage
 38 | 
 39 | .nyc_output
 40 | 
 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 42 | 
 43 | .grunt
 44 | 
 45 | # Bower dependency directory (https://bower.io/)
 46 | 
 47 | bower_components
 48 | 
 49 | # node-waf configuration
 50 | 
 51 | .lock-wscript
 52 | 
 53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 54 | 
 55 | build/Release
 56 | 
 57 | # Dependency directories
 58 | 
 59 | node_modules/
 60 | jspm_packages/
 61 | 
 62 | # Snowpack dependency directory (https://snowpack.dev/)
 63 | 
 64 | web_modules/
 65 | 
 66 | # TypeScript cache
 67 | 
 68 | *.tsbuildinfo
 69 | 
 70 | # Optional npm cache directory
 71 | 
 72 | .npm
 73 | 
 74 | # Optional eslint cache
 75 | 
 76 | .eslintcache
 77 | 
 78 | # Optional stylelint cache
 79 | 
 80 | .stylelintcache
 81 | 
 82 | # Microbundle cache
 83 | 
 84 | .rpt2_cache/
 85 | .rts2_cache_cjs/
 86 | .rts2_cache_es/
 87 | .rts2_cache_umd/
 88 | 
 89 | # Optional REPL history
 90 | 
 91 | .node_repl_history
 92 | 
 93 | # Output of 'npm pack'
 94 | 
 95 | *.tgz
 96 | 
 97 | # Yarn Integrity file
 98 | 
 99 | .yarn-integrity
100 | 
101 | # dotenv environment variable files
102 | 
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 | 
109 | # parcel-bundler cache (https://parceljs.org/)
110 | 
111 | .parcel-cache
112 | 
113 | # Next.js build output
114 | 
115 | .next
116 | out
117 | 
118 | # Nuxt.js build / generate output
119 | 
120 | .nuxt
121 | dist
122 | 
123 | # Gatsby files
124 | 
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 | 
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 | 
129 | # public
130 | 
131 | # vuepress build output
132 | 
133 | .vuepress/dist
134 | 
135 | # vuepress v2.x temp and cache directory
136 | 
137 | .temp
138 | 
139 | # Docusaurus cache and generated files
140 | 
141 | .docusaurus
142 | 
143 | # Serverless directories
144 | 
145 | .serverless/
146 | 
147 | # FuseBox cache
148 | 
149 | .fusebox/
150 | 
151 | # DynamoDB Local files
152 | 
153 | .dynamodb/
154 | 
155 | # TernJS port file
156 | 
157 | .tern-port
158 | 
159 | # Stores VSCode versions used for testing VSCode extensions
160 | 
161 | .vscode-test
162 | 
163 | # yarn v2
164 | 
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 | 
171 | # IntelliJ based IDEs
172 | .idea
173 | 
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Function Calling vs MCP Server
 2 | 
 3 | This repository is meant to illustrate the difference between LLM function calling and the Model Context Protocol (MCP).
 4 | Function calling has been around for a while, while MCP is a newer standardization attempt.
 5 | Comparing the two approaches showcases the value of MCP and how it builds on top of function calling.
 6 | 
 7 | This repository contains two examples:
 8 | 
 9 | - `/func-calling`: CLI app using OpenAI's function calling to control Home Assistant lights
10 | - `/mcp-server`: Node.js MCP server exposing a `control_lights` function to LLMs that use the MCP protocol
11 | 
12 | Want to see it in action? Check out my walkthrough on YouTube: [MCP vs. Function Calling - Controlling my office lights with Cursor](https://www.youtube.com/watch?v=DCp3SkPPq2A)
13 | 
14 | ## Home Assistant
15 | 
16 | [Home Assistant](https://www.home-assistant.io/) is an open-source home automation platform. I run it on a Raspberry Pi in my home.
17 | Home Assistant controls my lights, and you can control it via the Home Assistant WebSocket API.
18 | 
19 | I built out the `./data-manager` and `./hass-ws-client` utils while playing around with Home Assistant a while ago. I thought it would be a fun example for an external tool. However, the Home Assistant code isn't the focus of this repository.
20 | 
21 | ## Function Calling
22 | 
23 | [OpenAI function calling docs](https://platform.openai.com/docs/guides/function-calling)
24 | 
25 | Function calling lets AI assistants invoke predefined functions or tools. These functions run directly in the assistant's environment and can do anything from file searches to API calls. The LLM receives function descriptions in JSON format and specifies which function to call with what arguments. The application then handles the execution.
26 | 
27 | -> Functions live in your LLM application code.
28 | 
29 | ## MCP Server
30 | 
31 | [MCP docs](https://modelcontextprotocol.io/introduction)
32 | 
33 | MCP servers bridge AI applications with third-party services. They expose functions through a standardized protocol that any MCP-compatible LLM can use. While function calling happens locally, MCP servers handle external service communication, auth, and command execution separately.
34 | 
35 | -> MCP servers are standalone apps any MCP-compatible LLM can use.
36 | 
37 | ### Setting up the MCP server
38 | 
39 | 1. Create a `.env` file in the `mcp-server` directory:
40 | 
41 | ```bash
42 | cp mcp-server/.env.example mcp-server/.env
43 | ```
44 | 
45 | 2. Add your Home Assistant API token to the `.env` file:
46 | 
47 | ```bash
48 | HOME_ASSISTANT_API_TOKEN=<your-home-assistant-api-token>
49 | ```
50 | 
51 | 3. Build the MCP server:
52 | 
53 | ```bash
54 | bun i
55 | bun run build
56 | ```
57 | 
58 | 4. Add the MCP server to your LLM app config (e.g., Cursor):
59 | 
60 | ```json
61 | {
62 |   "name": "home-assistant",
63 |   "command": "node /Users/andrelandgraf/workspaces/mcps/mcp-server/dist/index.js"
64 | }
65 | ```
66 | 
67 | That's it! Your LLM app can now control Home Assistant lights through the MCP server.
68 | 
```

--------------------------------------------------------------------------------
/func-calling/src/data-manager/config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type AreaConfig = {
 2 |   areaId: string;
 3 | };
 4 | 
 5 | export const dashboardConfigs: AreaConfig[] = [
 6 |   {
 7 |     areaId: "living_room",
 8 |   },
 9 |   {
10 |     areaId: "kitchen",
11 |   },
12 |   {
13 |     areaId: "bedroom",
14 |   },
15 |   {
16 |     areaId: "office",
17 |   },
18 | ];
19 | 
```

--------------------------------------------------------------------------------
/mcp-server/src/data-manager/config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type AreaConfig = {
 2 |   areaId: string;
 3 | };
 4 | 
 5 | export const dashboardConfigs: AreaConfig[] = [
 6 |   {
 7 |     areaId: "living_room",
 8 |   },
 9 |   {
10 |     areaId: "kitchen",
11 |   },
12 |   {
13 |     areaId: "bedroom",
14 |   },
15 |   {
16 |     areaId: "office",
17 |   },
18 | ];
19 | 
```

--------------------------------------------------------------------------------
/func-calling/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "func-calling",
 3 |   "module": "index.ts",
 4 |   "type": "module",
 5 |   "devDependencies": {
 6 |     "@types/bun": "latest"
 7 |   },
 8 |   "peerDependencies": {
 9 |     "typescript": "^5.0.0"
10 |   },
11 |   "dependencies": {
12 |     "commander": "^13.1.0",
13 |     "inquirer": "^12.4.2",
14 |     "openai": "^4.86.1",
15 |     "tiny-invariant": "^1.3.3"
16 |   }
17 | }
18 | 
```

--------------------------------------------------------------------------------
/mcp-server/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "mcp-server",
 3 |   "module": "index.ts",
 4 |   "type": "module",
 5 |   "scripts": {
 6 |     "build": "bun build --target node --outfile dist/index.js --env inline src/index.ts"
 7 |   },
 8 |   "devDependencies": {
 9 |     "@types/bun": "latest"
10 |   },
11 |   "peerDependencies": {
12 |     "typescript": "^5.0.0"
13 |   },
14 |   "dependencies": {
15 |     "@modelcontextprotocol/sdk": "^1.6.1",
16 |     "tiny-invariant": "^1.3.3",
17 |     "ws": "^8.18.1",
18 |     "zod": "^3.24.2"
19 |   }
20 | }
21 | 
```

--------------------------------------------------------------------------------
/func-calling/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     // Enable latest features
 4 |     "lib": ["ESNext", "DOM"],
 5 |     "target": "ESNext",
 6 |     "module": "ESNext",
 7 |     "moduleDetection": "force",
 8 |     "jsx": "react-jsx",
 9 |     "allowJs": true,
10 | 
11 |     // Bundler mode
12 |     "moduleResolution": "bundler",
13 |     "allowImportingTsExtensions": true,
14 |     "verbatimModuleSyntax": true,
15 |     "noEmit": true,
16 | 
17 |     // Best practices
18 |     "strict": true,
19 |     "skipLibCheck": true,
20 |     "noFallthroughCasesInSwitch": true,
21 | 
22 |     // Some stricter flags (disabled by default)
23 |     "noUnusedLocals": false,
24 |     "noUnusedParameters": false,
25 |     "noPropertyAccessFromIndexSignature": false
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/mcp-server/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     // Enable latest features
 4 |     "lib": ["ESNext", "DOM"],
 5 |     "target": "ESNext",
 6 |     "module": "ESNext",
 7 |     "moduleDetection": "force",
 8 |     "jsx": "react-jsx",
 9 |     "allowJs": true,
10 | 
11 |     // Bundler mode
12 |     "moduleResolution": "bundler",
13 |     "allowImportingTsExtensions": true,
14 |     "verbatimModuleSyntax": true,
15 |     "noEmit": true,
16 | 
17 |     // Best practices
18 |     "strict": true,
19 |     "skipLibCheck": true,
20 |     "noFallthroughCasesInSwitch": true,
21 | 
22 |     // Some stricter flags (disabled by default)
23 |     "noUnusedLocals": false,
24 |     "noUnusedParameters": false,
25 |     "noPropertyAccessFromIndexSignature": false
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/func-calling/src/data-manager/data.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type Area = {
 2 |   id: string;
 3 |   name: string;
 4 |   floorId: string | null;
 5 |   lights: Light[];
 6 | };
 7 | 
 8 | export type Light = {
 9 |   areaId: string;
10 |   areaName: string;
11 |   deviceId: string | null;
12 |   deviceName: string;
13 |   entityId: string;
14 |   state: "on" | "off" | "unavailable";
15 |   brightnessPercentage: number | null;
16 |   rgbColor: [number, number, number] | null;
17 | };
18 | 
19 | export const EntityTypes = {
20 |   light: "light",
21 | } as const;
22 | 
23 | export type HomeAssistantData = {
24 |   areas: Area[];
25 | };
26 | 
27 | export function getLightState(state?: string): Light["state"] {
28 |   return state === "on" ? "on" : state === "off" ? "off" : "unavailable";
29 | }
30 | 
31 | export function getBrightnessPercentage(brightness: unknown): number | null {
32 |   const maxBrightness = 255;
33 |   let brightnessValue: number | null = null;
34 |   if (typeof brightness === "string") {
35 |     brightnessValue = Number.parseInt(brightness, 10);
36 |     if (Number.isNaN(brightnessValue)) {
37 |       return null;
38 |     }
39 |   } else if (typeof brightness === "number") {
40 |     brightnessValue = brightness;
41 |   } else {
42 |     return null;
43 |   }
44 |   return Math.round((brightnessValue / maxBrightness) * 100);
45 | }
46 | 
47 | export function getBrightnessValue(
48 |   brightnessPercentage: number | null,
49 | ): number {
50 |   if (brightnessPercentage === null) {
51 |     return 0;
52 |   }
53 |   const maxBrightness = 255;
54 |   return Math.round((brightnessPercentage / 100) * maxBrightness);
55 | }
56 | 
57 | export function getRBGColor(
58 |   rgbColor: unknown,
59 | ): [number, number, number] | null {
60 |   if (!rgbColor || !Array.isArray(rgbColor)) return null;
61 |   return rgbColor as [number, number, number];
62 | }
63 | 
```

--------------------------------------------------------------------------------
/mcp-server/src/data-manager/data.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export type Area = {
 2 |   id: string;
 3 |   name: string;
 4 |   floorId: string | null;
 5 |   lights: Light[];
 6 | };
 7 | 
 8 | export type Light = {
 9 |   areaId: string;
10 |   areaName: string;
11 |   deviceId: string | null;
12 |   deviceName: string;
13 |   entityId: string;
14 |   state: "on" | "off" | "unavailable";
15 |   brightnessPercentage: number | null;
16 |   rgbColor: [number, number, number] | null;
17 | };
18 | 
19 | export const EntityTypes = {
20 |   light: "light",
21 | } as const;
22 | 
23 | export type HomeAssistantData = {
24 |   areas: Area[];
25 | };
26 | 
27 | export function getLightState(state?: string): Light["state"] {
28 |   return state === "on" ? "on" : state === "off" ? "off" : "unavailable";
29 | }
30 | 
31 | export function getBrightnessPercentage(brightness: unknown): number | null {
32 |   const maxBrightness = 255;
33 |   let brightnessValue: number | null = null;
34 |   if (typeof brightness === "string") {
35 |     brightnessValue = Number.parseInt(brightness, 10);
36 |     if (Number.isNaN(brightnessValue)) {
37 |       return null;
38 |     }
39 |   } else if (typeof brightness === "number") {
40 |     brightnessValue = brightness;
41 |   } else {
42 |     return null;
43 |   }
44 |   return Math.round((brightnessValue / maxBrightness) * 100);
45 | }
46 | 
47 | export function getBrightnessValue(
48 |   brightnessPercentage: number | null,
49 | ): number {
50 |   if (brightnessPercentage === null) {
51 |     return 0;
52 |   }
53 |   const maxBrightness = 255;
54 |   return Math.round((brightnessPercentage / 100) * maxBrightness);
55 | }
56 | 
57 | export function getRBGColor(
58 |   rgbColor: unknown,
59 | ): [number, number, number] | null {
60 |   if (!rgbColor || !Array.isArray(rgbColor)) return null;
61 |   return rgbColor as [number, number, number];
62 | }
63 | 
```

--------------------------------------------------------------------------------
/mcp-server/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { HomeAssistantWebSocketClient } from "./hass-ws-client/client";
 2 | import { DataManager } from "./data-manager/data-manager";
 3 | import { dashboardConfigs } from "./data-manager/config";
 4 | import invariant from "tiny-invariant";
 5 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 7 | import { z } from "zod";
 8 | 
 9 | // Validate environment variables
10 | invariant(process.env.HOME_ASSISTANT_HOST, "HOME_ASSISTANT_HOST must be set");
11 | invariant(process.env.HOME_ASSISTANT_TOKEN, "HOME_ASSISTANT_TOKEN must be set");
12 | invariant(
13 |   process.env.HOME_ASSISTANT_SECURE,
14 |   "HOME_ASSISTANT_SECURE must be set",
15 | );
16 | 
17 | // Initialize Home Assistant client
18 | const hassClient = new HomeAssistantWebSocketClient(
19 |   process.env.HOME_ASSISTANT_HOST,
20 |   process.env.HOME_ASSISTANT_TOKEN,
21 |   {
22 |     isSecure: process.env.HOME_ASSISTANT_SECURE === "true",
23 |     shouldLog: false,
24 |   },
25 | );
26 | const dataManager = new DataManager(hassClient);
27 | dataManager.start();
28 | await new Promise((resolve) => setTimeout(resolve, 2000));
29 | 
30 | async function controlLight(params: {
31 |   areaId: string;
32 |   state: "on" | "off";
33 | }) {
34 |   if (params.state === "on") {
35 |     await dataManager.turnOnAllLights(params.areaId);
36 |   } else {
37 |     await dataManager.turnOffAllLights(params.areaId);
38 |   }
39 | }
40 | 
41 | // Define the light control schema
42 | const lightControlSchema = {
43 |   areaId: z
44 |     .string()
45 |     .describe(
46 |       "The area ID of the light in Home Assistant (e.g., office, kitchen)",
47 |     ),
48 |   state: z.enum(["on", "off"]).describe("Whether to turn the light on or off"),
49 | } as const;
50 | 
51 | // Create server instance
52 | const server = new McpServer({
53 |   name: "home-assistant",
54 |   version: "1.0.0",
55 | });
56 | 
57 | // Register the light control function
58 | server.tool(
59 |   "control_light",
60 |   "Control a light in Home Assistant (turn on/off)",
61 |   lightControlSchema,
62 |   async (params) => {
63 |     await controlLight(params);
64 |     return {
65 |       content: [
66 |         {
67 |           type: "text",
68 |           text: "Light control command executed successfully",
69 |         },
70 |       ],
71 |     };
72 |   },
73 | );
74 | 
75 | // Create transport and start server
76 | const transport = new StdioServerTransport();
77 | await server.connect(transport);
78 | 
79 | console.log("🏠 Home Assistant MCP Server Started!");
80 | console.log(
81 |   "Available areas:",
82 |   dashboardConfigs.map((config) => config.areaId),
83 | );
84 | 
```

--------------------------------------------------------------------------------
/func-calling/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import OpenAI from "openai";
  2 | import { HomeAssistantWebSocketClient } from "./hass-ws-client/client";
  3 | import invariant from "tiny-invariant";
  4 | import inquirer from "inquirer";
  5 | import { DataManager } from "./data-manager/data-manager";
  6 | import { dashboardConfigs } from "./data-manager/config";
  7 | 
  8 | // Validate environment variables
  9 | invariant(process.env.OPEN_AI_API_KEY, "OPEN_AI_API_KEY must be set");
 10 | invariant(process.env.HOME_ASSISTANT_HOST, "HOME_ASSISTANT_HOST must be set");
 11 | invariant(process.env.HOME_ASSISTANT_TOKEN, "HOME_ASSISTANT_TOKEN must be set");
 12 | invariant(
 13 |   process.env.HOME_ASSISTANT_SECURE,
 14 |   "HOME_ASSISTANT_SECURE must be set",
 15 | );
 16 | 
 17 | const openAiClient = new OpenAI({
 18 |   apiKey: process.env.OPEN_AI_API_KEY,
 19 | });
 20 | 
 21 | // Initialize Home Assistant client
 22 | const hassClient = new HomeAssistantWebSocketClient(
 23 |   process.env.HOME_ASSISTANT_HOST,
 24 |   process.env.HOME_ASSISTANT_TOKEN,
 25 |   {
 26 |     isSecure: process.env.HOME_ASSISTANT_SECURE === "true",
 27 |     shouldLog: false,
 28 |   },
 29 | );
 30 | const dataManager = new DataManager(hassClient);
 31 | dataManager.start();
 32 | await new Promise((resolve) => setTimeout(resolve, 2000));
 33 | 
 34 | const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
 35 |   {
 36 |     type: "function",
 37 |     function: {
 38 |       name: "control_light",
 39 |       description: "Control a light in Home Assistant (turn on/off)",
 40 |       parameters: {
 41 |         type: "object",
 42 |         properties: {
 43 |           areaId: {
 44 |             type: "string",
 45 |             description:
 46 |               "The area ID of the light in Home Assistant (e.g., office, kitchen)",
 47 |           },
 48 |           state: {
 49 |             type: "string",
 50 |             enum: ["on", "off"],
 51 |             description: "Whether to turn the light on or off",
 52 |           },
 53 |         },
 54 |         required: ["areaId", "state"],
 55 |         additionalProperties: false,
 56 |       },
 57 |       strict: true,
 58 |     },
 59 |   },
 60 | ];
 61 | 
 62 | // Get list of available area IDs
 63 | const availableAreaIds = dashboardConfigs
 64 |   .map((config) => config.areaId)
 65 |   .join(", ");
 66 | 
 67 | // Initialize chat history for OpenAI
 68 | const chatHistory: OpenAI.Chat.ChatCompletionMessageParam[] = [
 69 |   {
 70 |     role: "system",
 71 |     content: `Available area IDs in the system are: ${availableAreaIds}. If the user's request doesn't specify an area, ask them to specify one from this list.`,
 72 |   },
 73 | ];
 74 | 
 75 | async function controlLight(params: {
 76 |   areaId: string;
 77 |   state: "on" | "off";
 78 | }) {
 79 |   if (params.state === "on") {
 80 |     await dataManager.turnOnAllLights(params.areaId);
 81 |   } else {
 82 |     await dataManager.turnOffAllLights(params.areaId);
 83 |   }
 84 | }
 85 | 
 86 | async function processCommand(command: string) {
 87 |   try {
 88 |     // Add user's command to history
 89 |     chatHistory.push({
 90 |       role: "user",
 91 |       content: command,
 92 |     });
 93 | 
 94 |     const completion = await openAiClient.chat.completions.create({
 95 |       model: "gpt-4",
 96 |       messages: chatHistory,
 97 |       tools: tools,
 98 |     });
 99 | 
100 |     const replyText = completion.choices[0].message.content;
101 |     if (replyText) {
102 |       console.log("\n🤖 Assistant:", replyText);
103 |     }
104 | 
105 |     const toolCalls = completion.choices[0].message.tool_calls;
106 |     if (toolCalls) {
107 |       console.log("toolCalls", toolCalls);
108 |     }
109 |     if (toolCalls && toolCalls.length > 0) {
110 |       const call = toolCalls[0];
111 |       if (call.function.name === "control_light") {
112 |         const params = JSON.parse(call.function.arguments);
113 |         await controlLight(params);
114 | 
115 |         // Add the assistant's message with tool calls to chat history
116 |         chatHistory.push({
117 |           role: "assistant",
118 |           content: replyText,
119 |           tool_calls: toolCalls,
120 |         });
121 | 
122 |         // Add the tool response to chat history
123 |         const toolResponse: OpenAI.Chat.ChatCompletionMessageParam = {
124 |           role: "tool",
125 |           content: "Command executed successfully",
126 |           tool_call_id: call.id,
127 |         };
128 |         chatHistory.push(toolResponse);
129 |       }
130 |     }
131 |   } catch (error) {
132 |     const errorMessage = `Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`;
133 |     console.error("\nError processing command:", error);
134 | 
135 |     // Add the error message to chat history
136 |     chatHistory.push({
137 |       role: "developer",
138 |       content: errorMessage,
139 |     });
140 |   }
141 | }
142 | 
143 | async function main() {
144 |   console.log("🏠 Welcome to Home Assistant Light Control!");
145 |   console.log("Available areas:", availableAreaIds, "\n");
146 | 
147 |   while (true) {
148 |     const { command } = await inquirer.prompt([
149 |       {
150 |         type: "input",
151 |         name: "command",
152 |         message: "Enter your command:",
153 |       },
154 |     ]);
155 | 
156 |     await processCommand(command);
157 |     console.log(); // Empty line for better readability
158 |   }
159 | }
160 | 
161 | main().catch((error) => {
162 |   console.error("Fatal error:", error);
163 |   process.exit(1);
164 | });
165 | 
```

--------------------------------------------------------------------------------
/func-calling/src/data-manager/data-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   EntityTypes,
  3 |   getBrightnessPercentage,
  4 |   getBrightnessValue,
  5 |   getLightState,
  6 |   getRBGColor,
  7 |   type Light,
  8 |   type HomeAssistantData,
  9 | } from "./data";
 10 | import {
 11 |   type HassArea,
 12 |   type HassDevice,
 13 |   type HassEntity,
 14 |   type HassEntityState,
 15 |   HomeAssistantWebSocketClient,
 16 | } from "../hass-ws-client/client";
 17 | 
 18 | function getStateEntityDeviceForEntityId(
 19 |   entityId: string,
 20 |   devices: HassDevice[],
 21 |   entities: HassEntity[],
 22 |   entityStates: Record<string, HassEntityState>,
 23 | ) {
 24 |   const state = entityStates[entityId];
 25 |   const entity = entities.find((e) => e.entity_id === entityId);
 26 |   if (!entity) {
 27 |     throw Error(`Entity not found: ${entityId}`);
 28 |   }
 29 |   const device = devices.find((d) => d.id === entity.device_id);
 30 |   if (!device) {
 31 |     throw Error(`Device not found: ${entity.device_id} for entity ${entityId}`);
 32 |   }
 33 |   return { state, entity, device };
 34 | }
 35 | 
 36 | export class DataManager {
 37 |   private wsClient: HomeAssistantWebSocketClient;
 38 |   data: HomeAssistantData;
 39 |   incomingData: {
 40 |     areas: HassArea[] | null;
 41 |     devices: HassDevice[] | null;
 42 |     entities: HassEntity[] | null;
 43 |     entityStates: Record<string, HassEntityState> | null;
 44 |   } = {
 45 |     areas: null,
 46 |     devices: null,
 47 |     entities: null,
 48 |     entityStates: null,
 49 |   };
 50 | 
 51 |   constructor(wsClient: HomeAssistantWebSocketClient) {
 52 |     this.wsClient = wsClient;
 53 |     this.data = {
 54 |       areas: [],
 55 |     };
 56 |   }
 57 | 
 58 |   start() {
 59 |     this.wsClient.connect();
 60 |     this.wsClient.eventEmitter.on("areas", (areas) => {
 61 |       this.incomingData.areas = areas;
 62 |       this.syncData();
 63 |     });
 64 |     this.wsClient.eventEmitter.on("devices", (devices) => {
 65 |       this.incomingData.devices = devices;
 66 |       this.syncData();
 67 |     });
 68 |     this.wsClient.eventEmitter.on("entities", (entities) => {
 69 |       this.incomingData.entities = entities;
 70 |       this.syncData();
 71 |     });
 72 |     this.wsClient.eventEmitter.on("entity_states", (entitiesMap) => {
 73 |       this.incomingData.entityStates = entitiesMap;
 74 |       this.syncData();
 75 |     });
 76 |     this.wsClient.eventEmitter.on("entity_state_change", (changes) => {
 77 |       if (this.incomingData.entityStates) {
 78 |         for (const entityId of Object.keys(changes)) {
 79 |           const change = changes[entityId];
 80 |           const currentState = this.incomingData.entityStates[entityId];
 81 |           this.incomingData.entityStates[entityId] = {
 82 |             ...currentState,
 83 |             ...change["+"],
 84 |           };
 85 |         }
 86 |       } else {
 87 |         for (const entityId of Object.keys(changes)) {
 88 |           if (entityId.startsWith(`${EntityTypes.light}.`)) {
 89 |             const change = changes[entityId];
 90 |             this.updateLightState(entityId, change["+"]);
 91 |           }
 92 |         }
 93 |       }
 94 |     });
 95 |   }
 96 | 
 97 |   async cleanup() {
 98 |     this.wsClient.close();
 99 |   }
100 | 
101 |   private syncData() {
102 |     if (
103 |       this.incomingData.areas &&
104 |       this.incomingData.devices &&
105 |       this.incomingData.entities &&
106 |       this.incomingData.entityStates
107 |     ) {
108 |       this.updateAreas(this.incomingData.areas);
109 |       this.updateLights(
110 |         this.incomingData.devices,
111 |         this.incomingData.entities,
112 |         this.incomingData.entityStates,
113 |       );
114 |       this.incomingData.areas = null;
115 |       this.incomingData.devices = null;
116 |       this.incomingData.entities = null;
117 |       this.incomingData.entityStates = null;
118 |     }
119 |   }
120 | 
121 |   private updateAreas(areas: HassArea[]) {
122 |     const staleAreas = this.data.areas;
123 |     this.data.areas = areas.map((area) => {
124 |       const staleArea = staleAreas.find((a) => a.id === area.area_id);
125 |       return {
126 |         lights: [],
127 |         ...staleArea,
128 |         id: area.area_id,
129 |         name: area.name,
130 |         floorId: area.floor_id,
131 |       };
132 |     });
133 |   }
134 | 
135 |   private updateLights(
136 |     devices: HassDevice[],
137 |     entities: HassEntity[],
138 |     entityStates: Record<string, HassEntityState>,
139 |   ) {
140 |     for (const entityId of Object.keys(entityStates)) {
141 |       if (!entityId.startsWith(`${EntityTypes.light}.`)) {
142 |         continue;
143 |       }
144 |       const { state, device } = getStateEntityDeviceForEntityId(
145 |         entityId,
146 |         devices,
147 |         entities,
148 |         entityStates,
149 |       );
150 |       const area = this.data.areas.find((a) => a.id === device.area_id);
151 |       if (!area) {
152 |         throw Error(`Area not found: ${device.area_id} for light ${entityId}`);
153 |       }
154 |       const light: Light = {
155 |         areaId: area.id,
156 |         areaName: area.name,
157 |         deviceId: device.id,
158 |         deviceName: device.name,
159 |         entityId: entityId,
160 |         state: getLightState(state.s),
161 |         brightnessPercentage: getBrightnessPercentage(state.a.brightness),
162 |         rgbColor: getRBGColor(state.a.rgb_color),
163 |       };
164 |       const existingLightIndex = area.lights.findIndex(
165 |         (l) => l.entityId === entityId,
166 |       );
167 |       if (existingLightIndex !== -1) {
168 |         area.lights[existingLightIndex] = light;
169 |       } else {
170 |         area.lights.push(light);
171 |       }
172 |     }
173 |   }
174 | 
175 |   /**
176 |    * @returns {string | null} areaId of the area where the light is located or null if not found
177 |    */
178 |   private updateLightState(
179 |     entityId: string,
180 |     entityState: HassEntityState,
181 |   ): string | null {
182 |     for (const area of this.data.areas) {
183 |       const light = area.lights.find((l) => l.entityId === entityId);
184 |       if (light) {
185 |         light.state = getLightState(entityState.s);
186 |         if (entityState.a) {
187 |           light.brightnessPercentage = getBrightnessPercentage(
188 |             entityState.a.brightness,
189 |           );
190 |           light.rgbColor = getRBGColor(entityState.a.rgb_color);
191 |         }
192 |         return area.id;
193 |       }
194 |     }
195 |     return null;
196 |   }
197 | 
198 |   getLights(areaId: string) {
199 |     const area = this.data.areas.find((area) => area.id === areaId);
200 |     if (!area) {
201 |       throw new Error(`Area not found: ${areaId}`);
202 |     }
203 |     return area.lights;
204 |   }
205 | 
206 |   getAverageBrightness(areaId: string) {
207 |     const lights = this.getLights(areaId);
208 |     if (lights.length === 0) {
209 |       return 0;
210 |     }
211 |     const totalBrightness = lights.reduce(
212 |       (acc, light) => acc + (light.brightnessPercentage || 0),
213 |       0,
214 |     );
215 |     return totalBrightness / lights.length;
216 |   }
217 | 
218 |   turnOffLight(entityId: string) {
219 |     this.wsClient.sendTurnOffLight(entityId);
220 |   }
221 | 
222 |   turnOnLight(entityId: string) {
223 |     this.wsClient.sendTurnOnLight(entityId);
224 |   }
225 | 
226 |   dimLight(entityId: string, brightnessPercentage: number) {
227 |     const brightness = getBrightnessValue(brightnessPercentage);
228 |     if (brightness === null || brightness === 0) {
229 |       this.turnOffLight(entityId);
230 |     } else {
231 |       this.wsClient.sendTurnOnLight(entityId, { brightness });
232 |     }
233 |   }
234 | 
235 |   async turnOffAllLights(areaId: string) {
236 |     // console.log("Turning off all lights in area", areaId);
237 |     const lights = this.getLights(areaId);
238 |     for (const light of lights) {
239 |       if (light.state === "on") {
240 |         this.turnOffLight(light.entityId);
241 |       }
242 |     }
243 |   }
244 | 
245 |   async turnOnAllLights(areaId: string) {
246 |     const lights = this.getLights(areaId);
247 |     for (const light of lights) {
248 |       if (light.state === "off") {
249 |         this.turnOnLight(light.entityId);
250 |       }
251 |     }
252 |   }
253 | 
254 |   async dimAllLights(areaId: string, brightnessPercentage: number) {
255 |     const lights = this.getLights(areaId);
256 |     for (const light of lights) {
257 |       this.dimLight(light.entityId, brightnessPercentage);
258 |     }
259 |   }
260 | }
261 | 
```

--------------------------------------------------------------------------------
/mcp-server/src/data-manager/data-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   EntityTypes,
  3 |   getBrightnessPercentage,
  4 |   getBrightnessValue,
  5 |   getLightState,
  6 |   getRBGColor,
  7 |   type Light,
  8 |   type HomeAssistantData,
  9 | } from "./data";
 10 | import {
 11 |   type HassArea,
 12 |   type HassDevice,
 13 |   type HassEntity,
 14 |   type HassEntityState,
 15 |   HomeAssistantWebSocketClient,
 16 | } from "../hass-ws-client/client";
 17 | 
 18 | function getStateEntityDeviceForEntityId(
 19 |   entityId: string,
 20 |   devices: HassDevice[],
 21 |   entities: HassEntity[],
 22 |   entityStates: Record<string, HassEntityState>,
 23 | ) {
 24 |   const state = entityStates[entityId];
 25 |   const entity = entities.find((e) => e.entity_id === entityId);
 26 |   if (!entity) {
 27 |     throw Error(`Entity not found: ${entityId}`);
 28 |   }
 29 |   const device = devices.find((d) => d.id === entity.device_id);
 30 |   if (!device) {
 31 |     throw Error(`Device not found: ${entity.device_id} for entity ${entityId}`);
 32 |   }
 33 |   return { state, entity, device };
 34 | }
 35 | 
 36 | export class DataManager {
 37 |   private wsClient: HomeAssistantWebSocketClient;
 38 |   data: HomeAssistantData;
 39 |   incomingData: {
 40 |     areas: HassArea[] | null;
 41 |     devices: HassDevice[] | null;
 42 |     entities: HassEntity[] | null;
 43 |     entityStates: Record<string, HassEntityState> | null;
 44 |   } = {
 45 |     areas: null,
 46 |     devices: null,
 47 |     entities: null,
 48 |     entityStates: null,
 49 |   };
 50 | 
 51 |   constructor(wsClient: HomeAssistantWebSocketClient) {
 52 |     this.wsClient = wsClient;
 53 |     this.data = {
 54 |       areas: [],
 55 |     };
 56 |   }
 57 | 
 58 |   start() {
 59 |     this.wsClient.connect();
 60 |     this.wsClient.eventEmitter.on("areas", (areas) => {
 61 |       this.incomingData.areas = areas;
 62 |       this.syncData();
 63 |     });
 64 |     this.wsClient.eventEmitter.on("devices", (devices) => {
 65 |       this.incomingData.devices = devices;
 66 |       this.syncData();
 67 |     });
 68 |     this.wsClient.eventEmitter.on("entities", (entities) => {
 69 |       this.incomingData.entities = entities;
 70 |       this.syncData();
 71 |     });
 72 |     this.wsClient.eventEmitter.on("entity_states", (entitiesMap) => {
 73 |       this.incomingData.entityStates = entitiesMap;
 74 |       this.syncData();
 75 |     });
 76 |     this.wsClient.eventEmitter.on("entity_state_change", (changes) => {
 77 |       if (this.incomingData.entityStates) {
 78 |         for (const entityId of Object.keys(changes)) {
 79 |           const change = changes[entityId];
 80 |           const currentState = this.incomingData.entityStates[entityId];
 81 |           this.incomingData.entityStates[entityId] = {
 82 |             ...currentState,
 83 |             ...change["+"],
 84 |           };
 85 |         }
 86 |       } else {
 87 |         for (const entityId of Object.keys(changes)) {
 88 |           if (entityId.startsWith(`${EntityTypes.light}.`)) {
 89 |             const change = changes[entityId];
 90 |             this.updateLightState(entityId, change["+"]);
 91 |           }
 92 |         }
 93 |       }
 94 |     });
 95 |   }
 96 | 
 97 |   async cleanup() {
 98 |     this.wsClient.close();
 99 |   }
100 | 
101 |   private syncData() {
102 |     if (
103 |       this.incomingData.areas &&
104 |       this.incomingData.devices &&
105 |       this.incomingData.entities &&
106 |       this.incomingData.entityStates
107 |     ) {
108 |       this.updateAreas(this.incomingData.areas);
109 |       this.updateLights(
110 |         this.incomingData.devices,
111 |         this.incomingData.entities,
112 |         this.incomingData.entityStates,
113 |       );
114 |       this.incomingData.areas = null;
115 |       this.incomingData.devices = null;
116 |       this.incomingData.entities = null;
117 |       this.incomingData.entityStates = null;
118 |     }
119 |   }
120 | 
121 |   private updateAreas(areas: HassArea[]) {
122 |     const staleAreas = this.data.areas;
123 |     this.data.areas = areas.map((area) => {
124 |       const staleArea = staleAreas.find((a) => a.id === area.area_id);
125 |       return {
126 |         lights: [],
127 |         ...staleArea,
128 |         id: area.area_id,
129 |         name: area.name,
130 |         floorId: area.floor_id,
131 |       };
132 |     });
133 |   }
134 | 
135 |   private updateLights(
136 |     devices: HassDevice[],
137 |     entities: HassEntity[],
138 |     entityStates: Record<string, HassEntityState>,
139 |   ) {
140 |     for (const entityId of Object.keys(entityStates)) {
141 |       if (!entityId.startsWith(`${EntityTypes.light}.`)) {
142 |         continue;
143 |       }
144 |       const { state, device } = getStateEntityDeviceForEntityId(
145 |         entityId,
146 |         devices,
147 |         entities,
148 |         entityStates,
149 |       );
150 |       const area = this.data.areas.find((a) => a.id === device.area_id);
151 |       if (!area) {
152 |         throw Error(`Area not found: ${device.area_id} for light ${entityId}`);
153 |       }
154 |       const light: Light = {
155 |         areaId: area.id,
156 |         areaName: area.name,
157 |         deviceId: device.id,
158 |         deviceName: device.name,
159 |         entityId: entityId,
160 |         state: getLightState(state.s),
161 |         brightnessPercentage: getBrightnessPercentage(state.a.brightness),
162 |         rgbColor: getRBGColor(state.a.rgb_color),
163 |       };
164 |       const existingLightIndex = area.lights.findIndex(
165 |         (l) => l.entityId === entityId,
166 |       );
167 |       if (existingLightIndex !== -1) {
168 |         area.lights[existingLightIndex] = light;
169 |       } else {
170 |         area.lights.push(light);
171 |       }
172 |     }
173 |   }
174 | 
175 |   /**
176 |    * @returns {string | null} areaId of the area where the light is located or null if not found
177 |    */
178 |   private updateLightState(
179 |     entityId: string,
180 |     entityState: HassEntityState,
181 |   ): string | null {
182 |     for (const area of this.data.areas) {
183 |       const light = area.lights.find((l) => l.entityId === entityId);
184 |       if (light) {
185 |         light.state = getLightState(entityState.s);
186 |         if (entityState.a) {
187 |           light.brightnessPercentage = getBrightnessPercentage(
188 |             entityState.a.brightness,
189 |           );
190 |           light.rgbColor = getRBGColor(entityState.a.rgb_color);
191 |         }
192 |         return area.id;
193 |       }
194 |     }
195 |     return null;
196 |   }
197 | 
198 |   getLights(areaId: string) {
199 |     const area = this.data.areas.find((area) => area.id === areaId);
200 |     if (!area) {
201 |       throw new Error(`Area not found: ${areaId}`);
202 |     }
203 |     return area.lights;
204 |   }
205 | 
206 |   getAverageBrightness(areaId: string) {
207 |     const lights = this.getLights(areaId);
208 |     if (lights.length === 0) {
209 |       return 0;
210 |     }
211 |     const totalBrightness = lights.reduce(
212 |       (acc, light) => acc + (light.brightnessPercentage || 0),
213 |       0,
214 |     );
215 |     return totalBrightness / lights.length;
216 |   }
217 | 
218 |   turnOffLight(entityId: string) {
219 |     this.wsClient.sendTurnOffLight(entityId);
220 |   }
221 | 
222 |   turnOnLight(entityId: string) {
223 |     this.wsClient.sendTurnOnLight(entityId);
224 |   }
225 | 
226 |   dimLight(entityId: string, brightnessPercentage: number) {
227 |     const brightness = getBrightnessValue(brightnessPercentage);
228 |     if (brightness === null || brightness === 0) {
229 |       this.turnOffLight(entityId);
230 |     } else {
231 |       this.wsClient.sendTurnOnLight(entityId, { brightness });
232 |     }
233 |   }
234 | 
235 |   async turnOffAllLights(areaId: string) {
236 |     // console.log("Turning off all lights in area", areaId);
237 |     const lights = this.getLights(areaId);
238 |     for (const light of lights) {
239 |       if (light.state === "on") {
240 |         this.turnOffLight(light.entityId);
241 |       }
242 |     }
243 |   }
244 | 
245 |   async turnOnAllLights(areaId: string) {
246 |     const lights = this.getLights(areaId);
247 |     for (const light of lights) {
248 |       if (light.state === "off") {
249 |         this.turnOnLight(light.entityId);
250 |       }
251 |     }
252 |   }
253 | 
254 |   async dimAllLights(areaId: string, brightnessPercentage: number) {
255 |     const lights = this.getLights(areaId);
256 |     for (const light of lights) {
257 |       this.dimLight(light.entityId, brightnessPercentage);
258 |     }
259 |   }
260 | }
261 | 
```

--------------------------------------------------------------------------------
/func-calling/src/hass-ws-client/client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /* WebSocket client for the Home Assistant server */
  2 | import { WebSocket } from "ws";
  3 | import EventEmitter from "node:events";
  4 | import { clearInterval, setInterval } from "timers";
  5 | 
  6 | export type HassArea = {
  7 |   area_id: string; // unique name
  8 |   floor_id: string | null;
  9 |   name: string;
 10 | };
 11 | 
 12 | export type HassDevice = {
 13 |   area_id: string | null;
 14 |   id: string; // uuid
 15 |   manufacturer: string | null;
 16 |   model: string | null; // string not always usable
 17 |   name: string;
 18 |   name_by_user: string | null;
 19 | };
 20 | 
 21 | export type HassEntity = {
 22 |   device_id: string | null;
 23 |   entity_id: string; // unique name
 24 | };
 25 | 
 26 | export type HassEntityState = {
 27 |   s: string | "on" | "off" | "unavailable" | "not_home" | "home" | "unknown"; // state, number is string
 28 |   a: {
 29 |     [key: string]: unknown; // attributes
 30 |   };
 31 | };
 32 | 
 33 | export type HassHueLightEntityState = HassEntityState & {
 34 |   s: "on" | "off" | "unavailable";
 35 |   a: {
 36 |     color_mode: "color_temp" | string | null;
 37 |     brightness: number | null;
 38 |     color_temp_kelvin: number | null;
 39 |     color_temp: number | null;
 40 |     hs_color: number[] | null;
 41 |     rgb_color: number[] | null;
 42 |     xy_color: number[] | null;
 43 |   };
 44 | };
 45 | 
 46 | /**
 47 |  * Message types that Home Assistant server sends to the client
 48 |  */
 49 | const SERVER_MESSAGE_TYPES = {
 50 |   AUTH_REQUIRED: "auth_required",
 51 |   AUTH_OK: "auth_ok",
 52 |   AUTH_INVALID: "auth_invalid",
 53 |   RESULT: "result",
 54 |   EVENT: "event",
 55 | } as const;
 56 | 
 57 | /**
 58 |  * Message types that the client can send to the Home Assistant server
 59 |  */
 60 | const CLIENT_MESSAGE_TYPES = {
 61 |   AUTH: "auth",
 62 |   SUBSCRIBE_ENTITIES: "subscribe_entities",
 63 |   CALL_SERVICE: "call_service",
 64 |   GET_AREA_REGISTRY: "config/area_registry/list",
 65 |   GET_DEVICE_REGISTRY: "config/device_registry/list",
 66 |   GET_ENTITY_REGISTRY: "config/entity_registry/list",
 67 | } as const;
 68 | 
 69 | export type ClientMessageType =
 70 |   (typeof CLIENT_MESSAGE_TYPES)[keyof typeof CLIENT_MESSAGE_TYPES];
 71 | export type ServerMessageType =
 72 |   (typeof SERVER_MESSAGE_TYPES)[keyof typeof SERVER_MESSAGE_TYPES];
 73 | 
 74 | export class HomeAssistantWebSocketClient {
 75 |   private connectionUrl: string;
 76 |   private token: string;
 77 |   private socket: WebSocket | null = null;
 78 |   private shouldLog: boolean;
 79 |   private runningId = 1;
 80 |   private refetchInterval: ReturnType<typeof setInterval> | null = null;
 81 |   private ids = {
 82 |     areas: 0,
 83 |     devices: 0,
 84 |     entities: 0,
 85 |     entityStates: 0,
 86 |   };
 87 |   eventEmitter = new EventEmitter<{
 88 |     areas: [HassArea[]];
 89 |     devices: [HassDevice[]];
 90 |     entities: [HassEntity[]];
 91 |     entity_states: [Record<string, HassEntityState>];
 92 |     entity_state_change: [Record<string, { "+": HassEntityState }>];
 93 |   }>();
 94 | 
 95 |   constructor(
 96 |     host: string,
 97 |     token: string,
 98 |     { isSecure = false, shouldLog = false } = {},
 99 |   ) {
100 |     const protocol = isSecure ? "wss" : "ws";
101 |     this.connectionUrl = `${protocol}://${host}/api/websocket`;
102 |     this.token = token;
103 |     this.shouldLog = shouldLog;
104 |   }
105 | 
106 |   private log(message: string, ...args: unknown[]) {
107 |     if (this.shouldLog) {
108 |       console.log(`HomeAssistantWebSocketClient: ${message}`, ...args);
109 |     }
110 |   }
111 | 
112 |   connect() {
113 |     this.log("Connecting to Home Assistant WS server...");
114 |     if (this.socket) {
115 |       throw new Error("Socket unexpectedly already connected");
116 |     }
117 | 
118 |     this.runningId = 1;
119 |     const socket = new WebSocket(this.connectionUrl);
120 | 
121 |     socket.onopen = () => {
122 |       this.log("Connected to server");
123 |     };
124 | 
125 |     socket.onmessage = (event) => {
126 |       const data = JSON.parse(event.data.toString());
127 |       const serverMessageType = data.type;
128 |       this.log("Received message from Home Assistant: ", serverMessageType);
129 | 
130 |       if (serverMessageType === SERVER_MESSAGE_TYPES.AUTH_REQUIRED) {
131 |         socket.send(
132 |           JSON.stringify({
133 |             type: CLIENT_MESSAGE_TYPES.AUTH,
134 |             access_token: this.token,
135 |           }),
136 |         );
137 |         return;
138 |       }
139 | 
140 |       if (serverMessageType === SERVER_MESSAGE_TYPES.AUTH_INVALID) {
141 |         console.error("Authentication failed. Closing connection.");
142 |         socket.close();
143 |         return;
144 |       }
145 | 
146 |       if (serverMessageType === SERVER_MESSAGE_TYPES.AUTH_OK) {
147 |         this.log("Authentication successful.");
148 |         this.sendDataRequests();
149 |         return;
150 |       }
151 | 
152 |       if (serverMessageType === SERVER_MESSAGE_TYPES.RESULT) {
153 |         if (data.error) {
154 |           console.error("Error result: ", data.error);
155 |           return;
156 |         }
157 |         if (data.id === this.ids.areas) {
158 |           this.log("Received areas result", data.result.length);
159 |           this.eventEmitter.emit("areas", data.result);
160 |           return;
161 |         }
162 |         if (data.id === this.ids.devices) {
163 |           this.log("Received devices result", data.result.length);
164 |           this.eventEmitter.emit("devices", data.result);
165 |           return;
166 |         }
167 |         if (data.id === this.ids.entities) {
168 |           this.log("Received entities result", data.result.length);
169 |           this.eventEmitter.emit("entities", data.result);
170 |           return;
171 |         }
172 |         if (data.id === this.ids.entityStates) {
173 |           this.log("Successfully subscribed to entities");
174 |           return;
175 |         }
176 |         return;
177 |       }
178 | 
179 |       if (serverMessageType === SERVER_MESSAGE_TYPES.EVENT) {
180 |         if ("a" in data.event) {
181 |           this.log("Received entities event", Object.keys(data.event.a));
182 |           this.eventEmitter.emit("entity_states", data.event.a);
183 |           return;
184 |         }
185 |         if ("c" in data.event) {
186 |           this.log("Received entities change event", Object.keys(data.event.c));
187 |           this.eventEmitter.emit("entity_state_change", data.event.c);
188 |           return;
189 |         }
190 |         return;
191 |       }
192 |     };
193 | 
194 |     socket.onclose = () => {
195 |       this.log("Disconnected from server");
196 |     };
197 |     this.socket = socket;
198 | 
199 |     const threeMinutesInMs = 3 * 60 * 1000;
200 |     this.refetchInterval = setInterval(() => {
201 |       if (this.socket) {
202 |         this.sendDataRequests();
203 |       } else if (this.refetchInterval) {
204 |         clearInterval(this.refetchInterval);
205 |       }
206 |     }, threeMinutesInMs);
207 |   }
208 | 
209 |   close() {
210 |     if (this.socket) {
211 |       if (this.refetchInterval) {
212 |         clearInterval(this.refetchInterval);
213 |       }
214 |       this.socket.close();
215 |     }
216 |   }
217 | 
218 |   private send(type: ClientMessageType, payload?: object) {
219 |     if (!this.socket) {
220 |       throw new Error("Socket is not connected");
221 |     }
222 |     const id = this.runningId;
223 |     const message = { ...payload, type, id };
224 |     this.log("Sending ws message to Home Assistant: ", message);
225 |     this.socket.send(JSON.stringify(message));
226 |     this.runningId =
227 |       this.runningId + 1 >= Number.MAX_SAFE_INTEGER ? 1 : this.runningId + 1;
228 |     return id;
229 |   }
230 | 
231 |   private sendDataRequests() {
232 |     if (!this.socket) {
233 |       throw new Error(
234 |         "Attempting to sendDataRequests but socket is not connected",
235 |       );
236 |     }
237 |     this.ids.areas = this.send(CLIENT_MESSAGE_TYPES.GET_AREA_REGISTRY);
238 |     this.ids.devices = this.send(CLIENT_MESSAGE_TYPES.GET_DEVICE_REGISTRY);
239 |     this.ids.entities = this.send(CLIENT_MESSAGE_TYPES.GET_ENTITY_REGISTRY);
240 |     this.ids.entityStates = this.send(CLIENT_MESSAGE_TYPES.SUBSCRIBE_ENTITIES);
241 |   }
242 | 
243 |   sendToggleLight(entityId: string) {
244 |     this.log("Sending toggle light");
245 |     this.send(CLIENT_MESSAGE_TYPES.CALL_SERVICE, {
246 |       domain: "light",
247 |       service: "toggle",
248 |       service_data: { entity_id: entityId },
249 |     });
250 |   }
251 | 
252 |   sendTurnOnLight(entityId: string, data?: { brightness?: number }) {
253 |     this.log("Sending turn on light");
254 |     this.send(CLIENT_MESSAGE_TYPES.CALL_SERVICE, {
255 |       domain: "light",
256 |       service: "turn_on",
257 |       service_data: { entity_id: entityId, ...data },
258 |     });
259 |   }
260 | 
261 |   sendTurnOffLight(entityId: string) {
262 |     this.log("Sending turn off light");
263 |     this.send(CLIENT_MESSAGE_TYPES.CALL_SERVICE, {
264 |       domain: "light",
265 |       service: "turn_off",
266 |       service_data: { entity_id: entityId },
267 |     });
268 |   }
269 | }
270 | 
```

--------------------------------------------------------------------------------
/mcp-server/src/hass-ws-client/client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /* WebSocket client for the Home Assistant server */
  2 | import { WebSocket } from "ws";
  3 | import EventEmitter from "node:events";
  4 | import { clearInterval, setInterval } from "timers";
  5 | 
  6 | export type HassArea = {
  7 |   area_id: string; // unique name
  8 |   floor_id: string | null;
  9 |   name: string;
 10 | };
 11 | 
 12 | export type HassDevice = {
 13 |   area_id: string | null;
 14 |   id: string; // uuid
 15 |   manufacturer: string | null;
 16 |   model: string | null; // string not always usable
 17 |   name: string;
 18 |   name_by_user: string | null;
 19 | };
 20 | 
 21 | export type HassEntity = {
 22 |   device_id: string | null;
 23 |   entity_id: string; // unique name
 24 | };
 25 | 
 26 | export type HassEntityState = {
 27 |   s: string | "on" | "off" | "unavailable" | "not_home" | "home" | "unknown"; // state, number is string
 28 |   a: {
 29 |     [key: string]: unknown; // attributes
 30 |   };
 31 | };
 32 | 
 33 | export type HassHueLightEntityState = HassEntityState & {
 34 |   s: "on" | "off" | "unavailable";
 35 |   a: {
 36 |     color_mode: "color_temp" | string | null;
 37 |     brightness: number | null;
 38 |     color_temp_kelvin: number | null;
 39 |     color_temp: number | null;
 40 |     hs_color: number[] | null;
 41 |     rgb_color: number[] | null;
 42 |     xy_color: number[] | null;
 43 |   };
 44 | };
 45 | 
 46 | /**
 47 |  * Message types that Home Assistant server sends to the client
 48 |  */
 49 | const SERVER_MESSAGE_TYPES = {
 50 |   AUTH_REQUIRED: "auth_required",
 51 |   AUTH_OK: "auth_ok",
 52 |   AUTH_INVALID: "auth_invalid",
 53 |   RESULT: "result",
 54 |   EVENT: "event",
 55 | } as const;
 56 | 
 57 | /**
 58 |  * Message types that the client can send to the Home Assistant server
 59 |  */
 60 | const CLIENT_MESSAGE_TYPES = {
 61 |   AUTH: "auth",
 62 |   SUBSCRIBE_ENTITIES: "subscribe_entities",
 63 |   CALL_SERVICE: "call_service",
 64 |   GET_AREA_REGISTRY: "config/area_registry/list",
 65 |   GET_DEVICE_REGISTRY: "config/device_registry/list",
 66 |   GET_ENTITY_REGISTRY: "config/entity_registry/list",
 67 | } as const;
 68 | 
 69 | export type ClientMessageType =
 70 |   (typeof CLIENT_MESSAGE_TYPES)[keyof typeof CLIENT_MESSAGE_TYPES];
 71 | export type ServerMessageType =
 72 |   (typeof SERVER_MESSAGE_TYPES)[keyof typeof SERVER_MESSAGE_TYPES];
 73 | 
 74 | export class HomeAssistantWebSocketClient {
 75 |   private connectionUrl: string;
 76 |   private token: string;
 77 |   private socket: WebSocket | null = null;
 78 |   private shouldLog: boolean;
 79 |   private runningId = 1;
 80 |   private refetchInterval: ReturnType<typeof setInterval> | null = null;
 81 |   private ids = {
 82 |     areas: 0,
 83 |     devices: 0,
 84 |     entities: 0,
 85 |     entityStates: 0,
 86 |   };
 87 |   eventEmitter = new EventEmitter<{
 88 |     areas: [HassArea[]];
 89 |     devices: [HassDevice[]];
 90 |     entities: [HassEntity[]];
 91 |     entity_states: [Record<string, HassEntityState>];
 92 |     entity_state_change: [Record<string, { "+": HassEntityState }>];
 93 |   }>();
 94 | 
 95 |   constructor(
 96 |     host: string,
 97 |     token: string,
 98 |     { isSecure = false, shouldLog = false } = {},
 99 |   ) {
100 |     const protocol = isSecure ? "wss" : "ws";
101 |     this.connectionUrl = `${protocol}://${host}/api/websocket`;
102 |     this.token = token;
103 |     this.shouldLog = shouldLog;
104 |   }
105 | 
106 |   private log(message: string, ...args: unknown[]) {
107 |     if (this.shouldLog) {
108 |       console.log(`HomeAssistantWebSocketClient: ${message}`, ...args);
109 |     }
110 |   }
111 | 
112 |   connect() {
113 |     this.log("Connecting to Home Assistant WS server...");
114 |     if (this.socket) {
115 |       throw new Error("Socket unexpectedly already connected");
116 |     }
117 | 
118 |     this.runningId = 1;
119 |     const socket = new WebSocket(this.connectionUrl);
120 | 
121 |     socket.onopen = () => {
122 |       this.log("Connected to server");
123 |     };
124 | 
125 |     socket.onmessage = (event) => {
126 |       const data = JSON.parse(event.data.toString());
127 |       const serverMessageType = data.type;
128 |       this.log("Received message from Home Assistant: ", serverMessageType);
129 | 
130 |       if (serverMessageType === SERVER_MESSAGE_TYPES.AUTH_REQUIRED) {
131 |         socket.send(
132 |           JSON.stringify({
133 |             type: CLIENT_MESSAGE_TYPES.AUTH,
134 |             access_token: this.token,
135 |           }),
136 |         );
137 |         return;
138 |       }
139 | 
140 |       if (serverMessageType === SERVER_MESSAGE_TYPES.AUTH_INVALID) {
141 |         console.error("Authentication failed. Closing connection.");
142 |         socket.close();
143 |         return;
144 |       }
145 | 
146 |       if (serverMessageType === SERVER_MESSAGE_TYPES.AUTH_OK) {
147 |         this.log("Authentication successful.");
148 |         this.sendDataRequests();
149 |         return;
150 |       }
151 | 
152 |       if (serverMessageType === SERVER_MESSAGE_TYPES.RESULT) {
153 |         if (data.error) {
154 |           console.error("Error result: ", data.error);
155 |           return;
156 |         }
157 |         if (data.id === this.ids.areas) {
158 |           this.log("Received areas result", data.result.length);
159 |           this.eventEmitter.emit("areas", data.result);
160 |           return;
161 |         }
162 |         if (data.id === this.ids.devices) {
163 |           this.log("Received devices result", data.result.length);
164 |           this.eventEmitter.emit("devices", data.result);
165 |           return;
166 |         }
167 |         if (data.id === this.ids.entities) {
168 |           this.log("Received entities result", data.result.length);
169 |           this.eventEmitter.emit("entities", data.result);
170 |           return;
171 |         }
172 |         if (data.id === this.ids.entityStates) {
173 |           this.log("Successfully subscribed to entities");
174 |           return;
175 |         }
176 |         return;
177 |       }
178 | 
179 |       if (serverMessageType === SERVER_MESSAGE_TYPES.EVENT) {
180 |         if ("a" in data.event) {
181 |           this.log("Received entities event", Object.keys(data.event.a));
182 |           this.eventEmitter.emit("entity_states", data.event.a);
183 |           return;
184 |         }
185 |         if ("c" in data.event) {
186 |           this.log("Received entities change event", Object.keys(data.event.c));
187 |           this.eventEmitter.emit("entity_state_change", data.event.c);
188 |           return;
189 |         }
190 |         return;
191 |       }
192 |     };
193 | 
194 |     socket.onclose = () => {
195 |       this.log("Disconnected from server");
196 |     };
197 |     this.socket = socket;
198 | 
199 |     const threeMinutesInMs = 3 * 60 * 1000;
200 |     this.refetchInterval = setInterval(() => {
201 |       if (this.socket) {
202 |         this.sendDataRequests();
203 |       } else if (this.refetchInterval) {
204 |         clearInterval(this.refetchInterval);
205 |       }
206 |     }, threeMinutesInMs);
207 |   }
208 | 
209 |   close() {
210 |     if (this.socket) {
211 |       if (this.refetchInterval) {
212 |         clearInterval(this.refetchInterval);
213 |       }
214 |       this.socket.close();
215 |     }
216 |   }
217 | 
218 |   private send(type: ClientMessageType, payload?: object) {
219 |     if (!this.socket) {
220 |       throw new Error("Socket is not connected");
221 |     }
222 |     const id = this.runningId;
223 |     const message = { ...payload, type, id };
224 |     this.log("Sending ws message to Home Assistant: ", message);
225 |     this.socket.send(JSON.stringify(message));
226 |     this.runningId =
227 |       this.runningId + 1 >= Number.MAX_SAFE_INTEGER ? 1 : this.runningId + 1;
228 |     return id;
229 |   }
230 | 
231 |   private sendDataRequests() {
232 |     if (!this.socket) {
233 |       throw new Error(
234 |         "Attempting to sendDataRequests but socket is not connected",
235 |       );
236 |     }
237 |     this.ids.areas = this.send(CLIENT_MESSAGE_TYPES.GET_AREA_REGISTRY);
238 |     this.ids.devices = this.send(CLIENT_MESSAGE_TYPES.GET_DEVICE_REGISTRY);
239 |     this.ids.entities = this.send(CLIENT_MESSAGE_TYPES.GET_ENTITY_REGISTRY);
240 |     this.ids.entityStates = this.send(CLIENT_MESSAGE_TYPES.SUBSCRIBE_ENTITIES);
241 |   }
242 | 
243 |   sendToggleLight(entityId: string) {
244 |     this.log("Sending toggle light");
245 |     this.send(CLIENT_MESSAGE_TYPES.CALL_SERVICE, {
246 |       domain: "light",
247 |       service: "toggle",
248 |       service_data: { entity_id: entityId },
249 |     });
250 |   }
251 | 
252 |   sendTurnOnLight(entityId: string, data?: { brightness?: number }) {
253 |     this.log("Sending turn on light");
254 |     this.send(CLIENT_MESSAGE_TYPES.CALL_SERVICE, {
255 |       domain: "light",
256 |       service: "turn_on",
257 |       service_data: { entity_id: entityId, ...data },
258 |     });
259 |   }
260 | 
261 |   sendTurnOffLight(entityId: string) {
262 |     this.log("Sending turn off light");
263 |     this.send(CLIENT_MESSAGE_TYPES.CALL_SERVICE, {
264 |       domain: "light",
265 |       service: "turn_off",
266 |       service_data: { entity_id: entityId },
267 |     });
268 |   }
269 | }
270 | 
```