Skip to content

Commit 72cd3ca

Browse files
kingpanther13claude
andcommitted
Merge upstream/master into feat/settings-ui + address Patch76 review
Conflicts: - homeassistant-addon-dev/{config.yaml,translations/en.yaml,DOCS.md,start.py}: combined homeassistant-ai#1030 beta flags (filesystem/yaml/custom-component) with this PR's new dev-only options (tool_search_max_results, disabled_tools, pinned_tools). New options stay dev-only per homeassistant-ai#942 channel convention. - src/ha_mcp/server.py: combined homeassistant-ai#955's _apply_search_keyword_enrichment refactor with this PR's settings-visibility apply step. Order: tools -> enhanced -> skills -> _apply_settings_visibility -> _apply_search_keyword_enrichment -> _apply_tool_search. - homeassistant-addon/start.py: kept homeassistant-ai#806 migrate_skills_as_tools_default + relocated supervisor-token validation; added new env var exports. Patch76 review fixes: - G1: Mount settings UI under MCP secret_path so Docker/standalone clients share the same auth-by-obscurity as the MCP endpoint. Add-on continues to mount at root for HA ingress proxy. Routes don't register at all when neither path is available (stdio mode, or HTTP without secret). Moved register_settings_routes out of _initialize_server into the HTTP entry points (_run_http_server, _run_oauth_server, addon start.py). - G2: Wire tool_search_max_results through CategorizedSearchTransform; enforce 2-10 range in Pydantic Field and addon-dev schema int(2,10)?. - G3: 400 instead of 500 when POST body is JSON but not an object. - G4: Use SUPERVISOR_TOKEN, not /data existence, to detect add-on mode in _get_config_path. Matches the rest of the module. - G5: HTML-escape interpolated tool metadata in the settings JS. - G6: Comment explaining MANDATORY_TOOLS vs DEFAULT_PINNED_TOOLS overlap. - G7: Add ha_install_mcp_tools stub to FEATURE_GATED_TOOLS; rewrite stub copy to point at docs/beta.md (covers both stable and dev paths post-homeassistant-ai#942). - G9: Keep enable_yaml_config_editing guard with defense-in-depth comment; drop the discard so AND semantics apply (UI off OR toggle off -> tool off). - G12: Restore .env.example trailing newline. Tests cover non-dict body, garbage state values, route mounting under secret_path, _get_config_path env-driven path, FEATURE_GATED_TOOLS beta-system alignment, and the G9 AND-semantics regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents c44b323 + fb7f1a1 commit 72cd3ca

131 files changed

Lines changed: 16547 additions & 3675 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ LOG_LEVEL=INFO
4343

4444
# Optional: MCP Server Configuration
4545
MCP_SERVER_NAME=ha-mcp
46-
# MCP_SERVER_VERSION defaults to the package version (e.g. 6.7.2)
46+
# MCP_SERVER_VERSION defaults to the package version (e.g. 6.7.2)

.gemini/styleguide.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ All MCP tools MUST follow `ha_<verb>_<noun>` pattern:
7171
- `ha_set_*` — create/update operations
7272
- `ha_delete_*` — remove operations
7373
- `ha_call_*` — execute operations
74+
- `ha_manage_*` — multi-modal tools combining several operations behind one interface
7475

7576
Flag MEDIUM severity if tools don't follow this pattern.
7677

@@ -101,11 +102,12 @@ Flag HIGH severity if errors use plain exceptions or dict returns instead of str
101102
These rules apply to new or modified tool docstrings in the PR diff only -- not to pre-existing docstrings in unchanged files.
102103

103104
**Flag MEDIUM severity when a new or modified tool docstring:**
104-
- Does not start with an action verb (`Returns...` should be `Get...`; valid verbs: `Get`, `List`, `Search`, `Create`, `Update`, `Delete`, `Remove`, `Execute`, `Call`)
105+
- Does not start with an action verb (`Returns...` should be `Get...`; valid verbs: `Get`, `List`, `Search`, `Create`, `Update`, `Delete`, `Remove`, `Execute`, `Call`, `Manage`)
105106
- Is missing entirely or is still a placeholder
106107
- References a non-existent tool (e.g., `ha_get_domain_docs` -- the correct name is `ha_get_skill_home_assistant_best_practices`)
107108
- Embeds a full parameter schema instead of deferring to `ha_get_skill_home_assistant_best_practices`
108109
- Is a workflow-entry tool but gives no hint about the next natural tool to call
110+
- Multi-line docstring does not follow the structure template: (1) what the tool does, (2) when NOT to use it with preferred alternatives, (3) when to use it, (4) caveats. See AGENTS.md "Tool Docstrings" for details.
109111

110112
**Do NOT flag:**
111113
- Concise one-liners on straightforward tools (progressive disclosure: brief by default)

.github/workflows/dependabot-auto-merge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
steps:
1414
- name: Fetch Dependabot metadata
1515
id: metadata
16-
uses: dependabot/fetch-metadata@v2
16+
uses: dependabot/fetch-metadata@v3
1717
with:
1818
github-token: "${{ secrets.GITHUB_TOKEN }}"
1919

.github/workflows/deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup Node
3030
uses: actions/setup-node@v6
3131
with:
32-
node-version: "20"
32+
node-version: "22"
3333
cache: npm
3434
cache-dependency-path: site/package-lock.json
3535

@@ -43,7 +43,7 @@ jobs:
4343
run: npm run build
4444

4545
- name: Upload artifact
46-
uses: actions/upload-pages-artifact@v4
46+
uses: actions/upload-pages-artifact@v5
4747
with:
4848
path: ./site/dist
4949

.github/workflows/gemini-triage.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
steps:
3131
- name: Check if workflow should run
3232
id: check
33-
uses: actions/github-script@v8
33+
uses: actions/github-script@v9
3434
with:
3535
script: |
3636
let issue;
@@ -134,7 +134,7 @@ jobs:
134134

135135
- name: Get issue number
136136
id: issue_number
137-
uses: actions/github-script@v8
137+
uses: actions/github-script@v9
138138
with:
139139
script: |
140140
const issueNumber = context.payload.inputs?.issue_number || context.issue.number;
@@ -143,7 +143,7 @@ jobs:
143143
144144
- name: Post initial "working" comment
145145
id: initial_comment
146-
uses: actions/github-script@v8
146+
uses: actions/github-script@v9
147147
with:
148148
script: |
149149
const issueNumber = ${{ steps.issue_number.outputs.number }};
@@ -158,7 +158,7 @@ jobs:
158158
159159
- name: Fetch full issue thread
160160
id: issue_data
161-
uses: actions/github-script@v8
161+
uses: actions/github-script@v9
162162
with:
163163
script: |
164164
const issueNumber = ${{ steps.issue_number.outputs.number }};
@@ -207,7 +207,7 @@ jobs:
207207
- name: Build completeness check prompt
208208
id: build_prompt
209209
if: needs.should_run.outputs.skip_preanalysis != 'true'
210-
uses: actions/github-script@v8
210+
uses: actions/github-script@v9
211211
env:
212212
ISSUE_TYPE: ${{ steps.issue_data.outputs.issue_type }}
213213
ISSUE_TITLE: ${{ steps.issue_data.outputs.issue_title }}
@@ -533,7 +533,7 @@ jobs:
533533
534534
- name: Update comment and labels
535535
if: always() && steps.initial_comment.outputs.comment_id
536-
uses: actions/github-script@v8
536+
uses: actions/github-script@v9
537537
env:
538538
EVALUATE_RESULT: ${{ steps.evaluate.outputs.summary }}
539539
EVALUATE_ERROR: ${{ steps.evaluate.outputs.error }}

.github/workflows/release-publish.yml

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,100 @@ jobs:
2121
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
2222
outputs:
2323
version: ${{ steps.version.outputs.version }}
24+
release_tag: ${{ steps.version.outputs.release_tag }}
25+
should_publish: ${{ steps.version.outputs.should_publish }}
2426
steps:
2527
- uses: actions/checkout@v6
2628
with:
2729
ref: master
2830
fetch-depth: 0
2931

30-
- name: Extract version from pyproject.toml
32+
# Guard against the workflow_run trigger firing on odd-week SemVer Release
33+
# runs that were correctly gated out. A SemVer Release whose biweekly-gate
34+
# / check-changes / semantic-release jobs all skipped still ends with
35+
# conclusion=success (skipped != failed), which would otherwise republish
36+
# the existing stable version from master HEAD with whatever post-tag
37+
# commits have accumulated — same :stable / :latest label, different bits.
38+
# Publish only when master HEAD is at the latest stable tag's commit.
39+
- name: Verify master is at the latest stable tag
3140
id: version
3241
run: |
33-
VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
34-
echo "version=$VERSION" >> $GITHUB_OUTPUT
35-
echo "Release version: $VERSION"
42+
set -euo pipefail
43+
44+
# Resolve the latest stable tag. A real failure (corrupt repo, shallow
45+
# clone missing tags, etc.) should fail loudly — only exit-code 128
46+
# from `git describe` means "no matching tags exist in history" and is
47+
# benign for a fresh repo with no stable releases yet.
48+
set +e
49+
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*dev*' 2>&1)
50+
DESCRIBE_RC=$?
51+
set -e
52+
if [ "$DESCRIBE_RC" -eq 128 ]; then
53+
echo "::warning::No stable tag found; skipping Release Publish."
54+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
55+
echo "version=" >> "$GITHUB_OUTPUT"
56+
echo "release_tag=" >> "$GITHUB_OUTPUT"
57+
echo "## Release Publish skipped" >> "$GITHUB_STEP_SUMMARY"
58+
echo "No stable tag found in repository." >> "$GITHUB_STEP_SUMMARY"
59+
exit 0
60+
elif [ "$DESCRIBE_RC" -ne 0 ]; then
61+
echo "::error::git describe failed (rc=$DESCRIBE_RC): $LATEST_TAG"
62+
exit "$DESCRIBE_RC"
63+
fi
64+
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
65+
echo "Latest stable tag: $LATEST_TAG"
66+
67+
# Read the version from pyproject.toml AT THE TAG, not master HEAD, so
68+
# we publish what was tagged even if master has advanced. Prevents the
69+
# "workflow_dispatch bumped pyproject ahead of tag" corner case.
70+
VERSION=$(git show "$LATEST_TAG:pyproject.toml" \
71+
| grep '^version = ' | sed 's/version = "\(.*\)"/\1/')
72+
if [ -z "$VERSION" ]; then
73+
echo "::error::Could not parse version from $LATEST_TAG:pyproject.toml"
74+
exit 1
75+
fi
76+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
77+
echo "Tagged pyproject version: $VERSION"
78+
79+
TAG_SHA=$(git rev-list -n 1 "$LATEST_TAG")
80+
HEAD_SHA=$(git rev-parse HEAD)
81+
82+
# Normal release weeks: semver-release tags the release commit, then
83+
# pushes follow-up housekeeping commits to master (changelog sync,
84+
# addon version bump — all `[skip ci]`). HEAD != tag in that case,
85+
# but the release IS current. Mirror semver-release.yml's check-changes
86+
# logic: block only if there are releasable (feat/fix/perf) commits
87+
# on master since the tag that would have produced a new version.
88+
RELEASABLE=$(git log "$LATEST_TAG..HEAD" --oneline \
89+
--grep='^feat' --grep='^fix' --grep='^perf' || true)
90+
91+
if [ -n "$RELEASABLE" ]; then
92+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
93+
echo "::notice::Manual dispatch: unreleased feat/fix/perf commits exist on master since $LATEST_TAG. Forcing republish of $LATEST_TAG from the tag ref (master HEAD $HEAD_SHA is being ignored)."
94+
echo "## Release Publish — forced republish" >> "$GITHUB_STEP_SUMMARY"
95+
echo "Building \`$LATEST_TAG\` ($TAG_SHA); unreleased commits on master were ignored." >> "$GITHUB_STEP_SUMMARY"
96+
else
97+
echo "::notice::Unreleased feat/fix/perf commits exist on master since $LATEST_TAG; no new release to publish — skipping."
98+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
99+
echo "## Release Publish skipped" >> "$GITHUB_STEP_SUMMARY"
100+
echo "master has releasable commits since the latest stable tag \`$LATEST_TAG\` but no new tag was cut, so republishing \`$LATEST_TAG\` would be misleading." >> "$GITHUB_STEP_SUMMARY"
101+
echo "This is expected on biweekly-gate odd weeks; the re-triggered \`workflow_run\` is being correctly ignored." >> "$GITHUB_STEP_SUMMARY"
102+
exit 0
103+
fi
104+
fi
105+
106+
echo "should_publish=true" >> "$GITHUB_OUTPUT"
107+
echo "Release version: $VERSION (tag $LATEST_TAG)"
108+
echo "## Release Publish — $VERSION" >> "$GITHUB_STEP_SUMMARY"
109+
echo "Building from tag \`$LATEST_TAG\` ($TAG_SHA)." >> "$GITHUB_STEP_SUMMARY"
110+
if [ "$TAG_SHA" != "$HEAD_SHA" ]; then
111+
echo "(master HEAD is $HEAD_SHA — post-tag housekeeping commits only, no releasable changes.)" >> "$GITHUB_STEP_SUMMARY"
112+
fi
36113
37114
publish_pypi:
38115
name: Publish PyPI package
39116
needs: prepare
117+
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
40118
runs-on: ubuntu-latest
41119
environment:
42120
name: pypi
@@ -49,7 +127,7 @@ jobs:
49127
steps:
50128
- uses: actions/checkout@v6
51129
with:
52-
ref: master
130+
ref: ${{ needs.prepare.outputs.release_tag }}
53131
fetch-depth: 0
54132

55133
- name: Install uv
@@ -67,6 +145,7 @@ jobs:
67145
publish_docker:
68146
name: Publish Docker image
69147
needs: prepare
148+
if: ${{ needs.prepare.outputs.should_publish == 'true' }}
70149
runs-on: ubuntu-latest
71150
permissions:
72151
contents: read
@@ -76,7 +155,7 @@ jobs:
76155
steps:
77156
- uses: actions/checkout@v6
78157
with:
79-
ref: master
158+
ref: ${{ needs.prepare.outputs.release_tag }}
80159
fetch-depth: 0
81160

82161
- name: Set up Docker Buildx
@@ -121,7 +200,7 @@ jobs:
121200
name: Publish MCP server manifest
122201
needs: [publish_pypi, publish_docker, prepare]
123202
runs-on: ubuntu-latest
124-
if: ${{ always() }}
203+
if: ${{ always() && needs.prepare.outputs.should_publish == 'true' }}
125204
permissions:
126205
contents: read
127206
id-token: write # Required for MCP Registry OIDC authentication
@@ -137,7 +216,7 @@ jobs:
137216
- uses: actions/checkout@v6
138217
if: ${{ needs.publish_pypi.result == 'success' && needs.publish_docker.result == 'success' }}
139218
with:
140-
ref: master
219+
ref: ${{ needs.prepare.outputs.release_tag }}
141220
fetch-depth: 0
142221

143222
- name: Install MCP Publisher CLI

.github/workflows/renovate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
uses: actions/checkout@v6
3232

3333
- name: Self-hosted Renovate
34-
uses: renovatebot/github-action@v46.1.8
34+
uses: renovatebot/github-action@v46.1.11
3535
with:
3636
configurationFile: renovate.json
3737
token: ${{ secrets.GITHUB_TOKEN }}

AGENTS.md

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,27 +134,58 @@ gh issue list --state open --json number,title,labels --jq '.[] | select(.labels
134134
gh pr view <PR> --json comments --jq '.comments[] | {author: .author.login, created: .createdAt}'
135135

136136
# Check inline review comments (specific to code lines)
137-
gh api repos/homeassistant-ai/ha-mcp/pulls/<PR>/comments --jq '.[] | {path: .path, line: .line, author: .author.login, created_at: .created_at}'
137+
gh api repos/homeassistant-ai/ha-mcp/pulls/<PR>/comments --jq '.[] | {id, path, line, author: .user.login, created_at}'
138138

139139
# Check for unresolved review threads
140140
gh pr view <PR> --json reviews --jq '.reviews[] | select(.state == "COMMENTED") | .body'
141141
```
142142

143143
**Resolve threads:**
144-
After addressing a comment, **ALWAYS post a comment explaining the resolution, then mark the thread as resolved**:
144+
After addressing a comment, **ALWAYS post a comment explaining the resolution, then mark the thread as resolved**.
145+
146+
When there are inline review comments, do **both**: reply on each inline thread *and* post a PR-level review comment summarising the changes. The inline replies document the per-thread resolution where future readers expect it; the PR-level comment gives a single summary for anyone scanning the PR timeline.
147+
148+
**Always resolve the inline thread after replying**, unless the reply is asking the reviewer for further clarification (in which case leave the thread open so they can respond). An unresolved thread signals "still needs attention"; don't leave resolved work in that state. Unresolved threads also **block the PR from merging even after a maintainer has approved it** — the merge button stays disabled until every thread is marked resolved.
145149

146150
```bash
147-
# 1. FIRST: Post comment explaining what was done
148-
gh pr review <PR> --comment --body "✅ Fixed in [commit]. [Explanation]"
151+
# 1a. Reply on each inline thread via the /replies sub-endpoint.
152+
# <comment-id> is the numeric ID from:
153+
# gh api repos/homeassistant-ai/ha-mcp/pulls/<PR>/comments --jq '.[].id'
154+
gh api repos/homeassistant-ai/ha-mcp/pulls/<PR>/comments/<comment-id>/replies \
155+
-f body="✅ Fixed in [commit]. [Explanation]"
149156
# OR for dismissed suggestions:
150-
gh pr review <PR> --comment --body "📝 Not addressing because [reason]."
157+
gh api repos/homeassistant-ai/ha-mcp/pulls/<PR>/comments/<comment-id>/replies \
158+
-f body="📝 Not addressing because [reason]."
159+
160+
# 1b. Also post a PR-level review comment summarising the batch of changes:
161+
gh pr review <PR> --comment --body "✅ Addressed review feedback in [commit]. [Summary]"
162+
163+
# If there are no inline comments (just a general review), the PR-level
164+
# review comment alone is sufficient.
165+
166+
# 2. THEN: Resolve each thread. The GraphQL input field is `threadId` — NOT
167+
# `pullRequestReviewThreadId`, which GitHub rejects. The thread node ID
168+
# (PRRT_...) comes from a reviewThreads query; match databaseId against
169+
# the inline-comment numeric ID to pick the right one:
170+
gh api graphql -f query='
171+
query {
172+
repository(owner: "homeassistant-ai", name: "ha-mcp") {
173+
pullRequest(number: <PR>) {
174+
reviewThreads(first: 100) {
175+
nodes {
176+
id isResolved path line
177+
comments(first: 1) { nodes { databaseId } }
178+
}
179+
}
180+
}
181+
}
182+
}'
151183

152-
# 2. THEN: Resolve the thread
153184
gh api graphql -f query='mutation($threadId: ID!) {
154-
resolveReviewThread(input: {pullRequestReviewThreadId: $threadId}) {
185+
resolveReviewThread(input: {threadId: $threadId}) {
155186
thread { id isResolved }
156187
}
157-
}' -f threadId=<thread_id>
188+
}' -f threadId=<PRRT_...>
158189
```
159190

160191
**Why comment first:**
@@ -419,6 +450,7 @@ src/ha_mcp/
419450
- `delete` — delete dashboards, config entries, or files (`ha_config_delete_dashboard`, `ha_delete_file`)
420451
- `remove` — remove registry items (`ha_remove_entity`, `ha_config_remove_area`)
421452
- `call` — execute (`ha_call_service`)
453+
- `manage` — multi-modal tools combining several operations behind one interface (`ha_manage_addon`)
422454

423455
### Tool Structure
424456
Create `tools_<domain>.py` in `src/ha_mcp/tools/`. Registry auto-discovers it.
@@ -441,7 +473,8 @@ class DomainTools:
441473
# Add to the docstring above only when genuinely needed:
442474
# RELATED TOOLS: ha_next(): why to call this after (workflow-entry tools only)
443475
# EXAMPLES: ha_<verb>_<noun>("realistic_value") -- non-obvious call patterns only
444-
# NOTE / WARNING: non-obvious gotcha or destructive side-effect
476+
# When NOT to use: route to preferred alternatives
477+
# Caveats: destructive side-effects, non-obvious gotchas
445478
# For complex schemas: use ha_get_skill_home_assistant_best_practices
446479

447480
def register_<domain>_tools(mcp, client, **kwargs):
@@ -455,7 +488,7 @@ def register_<domain>_tools(mcp, client, **kwargs):
455488
The single-line template is the default -- extend it only where it genuinely helps.
456489

457490
**Required for every tool:**
458-
- Starts with an action verb (`Get`, `List`, `Search`, `Create`, `Update`, `Delete`, `Remove`, `Execute`, `Call`)
491+
- Starts with an action verb (`Get`, `List`, `Search`, `Create`, `Update`, `Delete`, `Remove`, `Execute`, `Call`, `Manage`)
459492
- One sentence describing what the tool does (not how)
460493

461494
**Add `RELATED TOOLS` when** the tool is a workflow entry point and the natural next step is not obvious.
@@ -464,8 +497,16 @@ Example: `ha_search_entities` hints at `ha_get_state`.
464497
**Add `EXAMPLES` when** the tool has multiple modes or non-obvious parameters.
465498
Omit when a single required parameter makes the call self-evident.
466499

467-
**Add `NOTE` or `WARNING` when** there is a non-obvious gotcha, a destructive side-effect,
468-
or a behavioral quirk that causes silent failures if ignored.
500+
**For multi-line docstrings, follow this structure** (based on
501+
[Anthropic's tool design guidance](https://www.anthropic.com/engineering/writing-tools-for-agents)):
502+
1. What the tool does (required first sentence, action verb)
503+
2. When NOT to use it — name the preferred alternatives
504+
3. When to use it — valid use cases
505+
4. Caveats — consequences, post-actions, destructive side-effects
506+
507+
Consequence statements are plain prose: "This permanently deletes the dashboard.
508+
A backup is created before every edit." Route safety concerns through `annotations`
509+
(`destructiveHint`, `idempotentHint`, `readOnlyHint`), not docstring keywords.
469510

470511
**Defer complex schemas** instead of embedding them:
471512
`# For complex schemas: use ha_get_skill_home_assistant_best_practices`

0 commit comments

Comments
 (0)