@@ -29,6 +29,14 @@ type CallServiceInput struct {
2929 EntityID string `json:"entity_id" jsonschema:"the entity ID (e.g., 'switch.switch_1')"`
3030}
3131
32+ type SearchEntitiesInput struct {
33+ Keyword string `json:"keyword" jsonschema:"search keyword to match in entity ID, domain, state, or friendly name"`
34+ }
35+
36+ type SearchServicesInput struct {
37+ Keyword string `json:"keyword" jsonschema:"search keyword to match in service domain or name"`
38+ }
39+
3240// Output types
3341type Entity struct {
3442 EntityID string `json:"entity_id" jsonschema:"the entity ID"`
@@ -64,6 +72,16 @@ type CallServiceOutput struct {
6472 Message string `json:"message" jsonschema:"status message"`
6573}
6674
75+ type SearchEntitiesOutput struct {
76+ Entities []Entity `json:"entities" jsonschema:"list of matching entities"`
77+ Count int `json:"count" jsonschema:"number of matching entities"`
78+ }
79+
80+ type SearchServicesOutput struct {
81+ Services []Service `json:"services" jsonschema:"list of matching services"`
82+ Count int `json:"count" jsonschema:"number of matching services"`
83+ }
84+
6785// ListEntities returns all entities in Home Assistant
6886func ListEntities (ctx context.Context , req * mcp.CallToolRequest , input ListEntitiesInput ) (
6987 * mcp.CallToolResult ,
@@ -190,6 +208,131 @@ func CallService(ctx context.Context, req *mcp.CallToolRequest, input CallServic
190208 return nil , output , nil
191209}
192210
211+ // SearchEntities searches for entities matching the keyword
212+ func SearchEntities (ctx context.Context , req * mcp.CallToolRequest , input SearchEntitiesInput ) (
213+ * mcp.CallToolResult ,
214+ SearchEntitiesOutput ,
215+ error ,
216+ ) {
217+ states , err := client .GetStates (ctx )
218+ if err != nil {
219+ return nil , SearchEntitiesOutput {}, fmt .Errorf ("failed to get states: %w" , err )
220+ }
221+
222+ keyword := strings .ToLower (input .Keyword )
223+ var matchingEntities []Entity
224+
225+ for _ , state := range states {
226+ // Extract domain from entity ID
227+ data := strings .Split (state .EntityId , "." )
228+ domain := ""
229+ if len (data ) > 0 {
230+ domain = data [0 ]
231+ }
232+
233+ // Check if keyword matches in entity ID
234+ entityIDMatch := strings .Contains (strings .ToLower (state .EntityId ), keyword )
235+
236+ // Check if keyword matches in domain
237+ domainMatch := strings .Contains (strings .ToLower (domain ), keyword )
238+
239+ // Check if keyword matches in state
240+ stateMatch := strings .Contains (strings .ToLower (state .State ), keyword )
241+
242+ // Check if keyword matches in friendly name
243+ friendlyNameMatch := false
244+ if friendlyName , ok := state .Attributes ["friendly_name" ].(string ); ok {
245+ friendlyNameMatch = strings .Contains (strings .ToLower (friendlyName ), keyword )
246+ }
247+
248+ // If keyword matches in any field, include this entity
249+ if entityIDMatch || domainMatch || stateMatch || friendlyNameMatch {
250+ friendlyName := state .Attributes ["friendly_name" ]
251+ entity := Entity {
252+ EntityID : state .EntityId ,
253+ State : state .State ,
254+ FriendlyName : friendlyName ,
255+ Domain : domain ,
256+ }
257+ matchingEntities = append (matchingEntities , entity )
258+ }
259+ }
260+
261+ output := SearchEntitiesOutput {
262+ Entities : matchingEntities ,
263+ Count : len (matchingEntities ),
264+ }
265+
266+ return nil , output , nil
267+ }
268+
269+ // SearchServices searches for services matching the keyword
270+ func SearchServices (ctx context.Context , req * mcp.CallToolRequest , input SearchServicesInput ) (
271+ * mcp.CallToolResult ,
272+ SearchServicesOutput ,
273+ error ,
274+ ) {
275+ services , err := client .GetServices (ctx )
276+ if err != nil {
277+ return nil , SearchServicesOutput {}, fmt .Errorf ("failed to get services: %w" , err )
278+ }
279+
280+ keyword := strings .ToLower (input .Keyword )
281+ var result []Service
282+
283+ for _ , s := range services {
284+ // Check if keyword matches in domain
285+ domainMatch := strings .Contains (strings .ToLower (s .Domain ), keyword )
286+
287+ for serviceName , serviceInfo := range s .Services {
288+ // Check if keyword matches in service name
289+ serviceNameMatch := strings .Contains (strings .ToLower (serviceName ), keyword )
290+
291+ // If keyword matches in domain or service name, include this service
292+ if domainMatch || serviceNameMatch {
293+ // Convert service fields to our format
294+ fields := make (map [string ]ServiceField )
295+ for fieldName , fieldInfo := range serviceInfo .Fields {
296+ field := ServiceField {
297+ Description : fieldInfo .Description ,
298+ }
299+
300+ // Set example if available (convert to string if needed)
301+ if fieldInfo .Example != nil {
302+ switch v := fieldInfo .Example .(type ) {
303+ case string :
304+ field .Example = v
305+ default :
306+ field .Example = fmt .Sprintf ("%v" , v )
307+ }
308+ }
309+
310+ // Set required flag if available
311+ if fieldInfo .Selector != nil {
312+ field .Required = true
313+ }
314+
315+ fields [fieldName ] = field
316+ }
317+
318+ service := Service {
319+ Domain : s .Domain ,
320+ Name : serviceName ,
321+ Fields : fields ,
322+ }
323+ result = append (result , service )
324+ }
325+ }
326+ }
327+
328+ output := SearchServicesOutput {
329+ Services : result ,
330+ Count : len (result ),
331+ }
332+
333+ return nil , output , nil
334+ }
335+
193336func main () {
194337 // Get configuration from environment variables
195338 token := os .Getenv ("HA_TOKEN" )
@@ -239,6 +382,16 @@ func main() {
239382 Description : "Call a service in Home Assistant (e.g., turn_on, turn_off, toggle)" ,
240383 }, CallService )
241384
385+ mcp .AddTool (server , & mcp.Tool {
386+ Name : "search_entities" ,
387+ Description : "Search for entities in Home Assistant by keyword (searches across entity ID, domain, state, and friendly name)" ,
388+ }, SearchEntities )
389+
390+ mcp .AddTool (server , & mcp.Tool {
391+ Name : "search_services" ,
392+ Description : "Search for services in Home Assistant by keyword (searches across service domain and name)" ,
393+ }, SearchServices )
394+
242395 // Run the server
243396 if err := server .Run (context .Background (), & mcp.StdioTransport {}); err != nil {
244397 log .Fatal (err )
0 commit comments