Skip to content

feat: add Render deployment for multi-device Claude access#98

Open
boeresteven-bit wants to merge 6 commits into
mvilanova:mainfrom
boeresteven-bit:claude/modest-wilson
Open

feat: add Render deployment for multi-device Claude access#98
boeresteven-bit wants to merge 6 commits into
mvilanova:mainfrom
boeresteven-bit:claude/modest-wilson

Conversation

@boeresteven-bit
Copy link
Copy Markdown

Summary

  • Voegt design spec en implementatieplan toe voor Render deployment
  • Fixes FASTMCP_HOST binding (server bindt nu aan 0.0.0.0 in container)
  • Fixes Dockerfile CMD zodat MCP_TRANSPORT env var gerespecteerd wordt
  • Maakt Claude toegankelijk op mobiel en andere apparaten via SSE

Test plan

  • Code changes uit implementatieplan toepassen
  • Render health check verifieëren op /sse
  • Bevestigen dat Claude mobile verbinding maakt en tools zichtbaar zijn
  • pytest draaien voor regressiecheck

🤖 Generated with Claude Code

Steven Boere and others added 6 commits March 14, 2026 22:23
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mvilanova mvilanova requested a review from Copilot March 27, 2026 15:59
@boeresteven-bit boeresteven-bit temporarily deployed to intervals-mcp-server-env March 27, 2026 15:59 — with GitHub Actions Inactive
@mvilanova
Copy link
Copy Markdown
Owner

Could you please resolve the conflicts?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables deploying intervals-mcp-server to Render for remote (multi-device) Claude access over SSE, and includes supporting docs plus a few UX tweaks around “Unnamed” activities.

Changes:

  • Read FASTMCP_HOST from the environment when constructing the FastMCP instance (with tests to verify behavior).
  • Update Dockerfile entrypoint to run server.py directly so MCP_TRANSPORT selection in __main__ is applied.
  • Improve handling/display of “Unnamed” activities by falling back to activity type in summaries and adjusting filtering logic.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/intervals_mcp_server/server.py Pass host= from FASTMCP_HOST when creating FastMCP.
tests/test_server_config.py Adds tests ensuring FASTMCP_HOST is respected.
Dockerfile Switches CMD to python src/intervals_mcp_server/server.py.
src/intervals_mcp_server/utils/formatting.py Formats activity name using type fallback when name is empty/“Unnamed”.
src/intervals_mcp_server/tools/activities.py Adjusts filtering so activities with a known type aren’t dropped due to “Unnamed”.
docs/superpowers/specs/2026-03-24-render-deployment-design.md Design spec for Render SSE deployment.
docs/superpowers/plans/2026-03-24-render-deployment.md Step-by-step implementation/deployment plan.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Dockerfile
@@ -23,4 +23,4 @@ COPY .env.example .env.example
RUN pip install --no-cache-dir .

# Default command to run the MCP server using stdio transport
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this is the default command to run the server using stdio transport, but the entrypoint now runs server.py which selects transport via MCP_TRANSPORT. Update the comment to avoid misleading future changes/debugging.

Suggested change
# Default command to run the MCP server using stdio transport
# Default command to run the MCP server; transport is selected by server.py via MCP_TRANSPORT

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +19
Deploy as a Render Web Service using the existing Dockerfile, with `MCP_TRANSPORT=sse`. Claude (web and mobile) supports remote MCP servers via SSE. The Render-provided HTTPS URL is used directly as the MCP server URL in Claude's integrations settings.

Security relies on URL obscurity (the Render subdomain is not guessable). No additional authentication is added.

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relying on an unguessable URL as the only access control means anyone who obtains the Render URL can call MCP tools and indirectly access the athlete’s data using the server-side API_KEY. Consider documenting this as a concrete risk and adding at least an optional shared-secret/bearer-token check (or Render-level auth) for safer defaults when deployed publicly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@boeresteven-bit this seems like too big a security risk. Can you move the API key and Athlete id to the claude mcp config file and not the render web app?

Comment on lines +70 to +73
```bash
cd /Users/stevenboere/intervals-mcp-server/.claude/worktrees/modest-wilson
uv run pytest tests/test_server_config.py -v
```
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plan embeds an absolute local filesystem path in the test command. That’s not portable and may leak personal machine details into the repo; use a repo-relative command (e.g., run from the project root) instead.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +20
for key in list(sys.modules.keys()):
if "intervals_mcp_server" in key:
del sys.modules[key]
import intervals_mcp_server.server as srv # pylint: disable=import-outside-toplevel
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting every module whose name contains "intervals_mcp_server" can create multiple live module instances (old objects still referenced by imported functions, while monkeypatch/setattr later may import a fresh module), which can make the suite order-dependent/flaky. Prefer reloading only intervals_mcp_server.server (e.g., via importlib.reload) or save/restore the affected sys.modules entries so other tests keep patching the same module objects.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +61
activity_type = activity.get("type", "")

# Accept activities with a meaningful custom name
if name and name != "Unnamed":
return True

# Also accept activities that have a known type, even if their name is
# "Unnamed" or empty – these are real workouts that were just never
# renamed (common for auto-synced Garmin activities).
if str(activity_type).lower() not in UNKNOWN_TYPES and activity_type != "Unknown":
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNKNOWN_TYPES includes None, but the check uses str(activity_type).lower(). If activity_type is None, this becomes "none" and the condition incorrectly treats it as a known type, keeping unnamed activities with no real type. Handle None/non-string types explicitly (e.g., normalize to "" first or check activity_type is None before calling str(...).lower()).

Suggested change
activity_type = activity.get("type", "")
# Accept activities with a meaningful custom name
if name and name != "Unnamed":
return True
# Also accept activities that have a known type, even if their name is
# "Unnamed" or empty – these are real workouts that were just never
# renamed (common for auto-synced Garmin activities).
if str(activity_type).lower() not in UNKNOWN_TYPES and activity_type != "Unknown":
activity_type = activity.get("type")
# Accept activities with a meaningful custom name
if name and name != "Unnamed":
return True
# Normalize activity type: only lower-case strings; treat None/non-strings as unknown.
if isinstance(activity_type, str):
normalized_type = activity_type.strip().lower()
else:
normalized_type = ""
# Also accept activities that have a known type, even if their name is
# "Unnamed" or empty – these are real workouts that were just never
# renamed (common for auto-synced Garmin activities).
if normalized_type not in UNKNOWN_TYPES and activity_type != "Unknown":

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +47
"""Filter out unnamed activities from the list.

Keeps activities that either have a real name, or have a known activity type
(e.g. Ride, Run, VirtualRide) even if they were not explicitly named by the user.
This prevents real cycling or running activities from being dropped just because
the user left the default "Unnamed" label in Garmin/Strava.
"""
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function now keeps activities that are still effectively unnamed (e.g., name == "Unnamed") if they have a type, which changes the meaning of include_unnamed=False and makes _filter_named_activities (and related user-facing messaging like "No named activities found") inaccurate. Consider renaming this filter and/or updating the include_unnamed parameter docs/messages to reflect the new behavior (e.g., "include activities with no name and no type").

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +37
raw_name = activity.get("name", "")
activity_type = activity.get("type", "")
display_name = raw_name if (raw_name and raw_name != "Unnamed") else (activity_type or "Unnamed")

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new display_name behavior (falling back to activity type when name is empty/"Unnamed") isn’t covered by tests. Since tests/test_formatting.py already asserts activity name formatting, add a case for an unnamed activity to prevent regressions (e.g., name="Unnamed", type="Ride" should render as "Activity: Ride").

Copilot uses AI. Check for mistakes.
@eoinoconn
Copy link
Copy Markdown
Collaborator

Awesome work @boeresteven-bit could you update the README to describe how to set it up?

"""
UNKNOWN_TYPES = {"", "unknown", None}

def _is_valid_activity(activity: dict[str, Any]) -> bool:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of function definitions inside function definitions. Especially for a function like this that could have wider use in the future.

Comment on lines +16 to +19
Deploy as a Render Web Service using the existing Dockerfile, with `MCP_TRANSPORT=sse`. Claude (web and mobile) supports remote MCP servers via SSE. The Render-provided HTTPS URL is used directly as the MCP server URL in Claude's integrations settings.

Security relies on URL obscurity (the Render subdomain is not guessable). No additional authentication is added.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@boeresteven-bit this seems like too big a security risk. Can you move the API key and Athlete id to the claude mcp config file and not the render web app?

"""
UNKNOWN_TYPES = {"", "unknown", None}

def _is_valid_activity(activity: dict[str, Any]) -> bool:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of function definitions within function defintions. Harder to test. In this case the functionis_valid_activity may have use outside this function in a future iteration. Id move this out of the outer function.

@@ -0,0 +1,218 @@
# Render Deployment Implementation Plan
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With so many AI tools out there I'm cautious about letting these spec and plan files pile up. Happy to leave them here for now but something to keep in mind in the future. Perhaps well revist if there's too many.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

# Also accept activities that have a known type, even if their name is
# "Unnamed" or empty – these are real workouts that were just never
# renamed (common for auto-synced Garmin activities).
if str(activity_type).lower() not in UNKNOWN_TYPES and activity_type != "Unknown":
Comment on lines +41 to +44
"""Filter out unnamed activities from the list.

Keeps activities that either have a real name, or have a known activity type
(e.g. Ride, Run, VirtualRide) even if they were not explicitly named by the user.
Comment on lines +27 to +29
for key in list(sys.modules.keys()):
if "intervals_mcp_server" in key:
del sys.modules[key]
Comment on lines +178 to +182
| `MCP_TRANSPORT` | `sse` |
| `FASTMCP_HOST` | `0.0.0.0` |
| `ATHLETE_ID` | your Intervals.icu athlete ID (e.g. `i12345`) |
| `API_KEY` | your Intervals.icu API key |
| `INTERVALS_API_BASE_URL` | `https://intervals.icu/api/v1` |
Comment on lines +58 to +62
# Also accept activities that have a known type, even if their name is
# "Unnamed" or empty – these are real workouts that were just never
# renamed (common for auto-synced Garmin activities).
if str(activity_type).lower() not in UNKNOWN_TYPES and activity_type != "Unknown":
return True
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants