hevy-coach 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +42 -146
- package/dist/cli.mjs +16 -0
- package/dist/http.mjs +133 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +2 -0
- package/dist/src-BxQx3e_u.mjs +651 -0
- package/package.json +32 -33
- package/CHATGPT.md +0 -388
- package/GEMINI.md +0 -416
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -16
- package/dist/cli.js.map +0 -1
- package/dist/http.d.ts +0 -3
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -174
- package/dist/http.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -23
- package/dist/index.js.map +0 -1
- package/dist/tools/body-measurements.d.ts +0 -4
- package/dist/tools/body-measurements.d.ts.map +0 -1
- package/dist/tools/body-measurements.js +0 -88
- package/dist/tools/body-measurements.js.map +0 -1
- package/dist/tools/coaching.d.ts +0 -4
- package/dist/tools/coaching.d.ts.map +0 -1
- package/dist/tools/coaching.js +0 -353
- package/dist/tools/coaching.js.map +0 -1
- package/dist/tools/exercises.d.ts +0 -4
- package/dist/tools/exercises.d.ts.map +0 -1
- package/dist/tools/exercises.js +0 -121
- package/dist/tools/exercises.js.map +0 -1
- package/dist/tools/routines.d.ts +0 -4
- package/dist/tools/routines.d.ts.map +0 -1
- package/dist/tools/routines.js +0 -150
- package/dist/tools/routines.js.map +0 -1
- package/dist/tools/users.d.ts +0 -4
- package/dist/tools/users.d.ts.map +0 -1
- package/dist/tools/users.js +0 -16
- package/dist/tools/users.js.map +0 -1
- package/dist/tools/workouts.d.ts +0 -4
- package/dist/tools/workouts.d.ts.map +0 -1
- package/dist/tools/workouts.js +0 -134
- package/dist/tools/workouts.js.map +0 -1
- package/dist/utils/hevy-client.d.ts +0 -212
- package/dist/utils/hevy-client.d.ts.map +0 -1
- package/dist/utils/hevy-client.js +0 -178
- package/dist/utils/hevy-client.js.map +0 -1
- package/dist/utils/response.d.ts +0 -21
- package/dist/utils/response.d.ts.map +0 -1
- package/dist/utils/response.js +0 -22
- package/dist/utils/response.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- Node 22.18+ required.
|
|
8
|
+
- `hevy-coach-http` requires `MCP_AUTH_TOKEN`, binds to `127.0.0.1` by default, and needs `MCP_ALLOWED_HOSTS` to bind elsewhere. CORS is off unless `MCP_ALLOWED_ORIGINS` lists browser origins.
|
|
9
|
+
- `create-body-measurement` and `update-body-measurement` replaced by `log-body-measurement` (create or overwrite).
|
|
10
|
+
- `create-routine` and `update-routine` take `folder_id` (snake_case). `update-routine` moves a routine when `folder_id` is given (null = "My Routines") and keeps the folder when it is omitted. `rpe` is no longer accepted on routine sets (Hevy rejects it).
|
|
11
|
+
- Error results include Hevy's HTTP status, error code and message instead of a generic axios message.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `get-exercise-progression` threw on live data: exercise history is flat (one row per set), not nested.
|
|
16
|
+
- `get-workout` and `analyze-workout` returned "not found" for valid workouts because the response was assumed to be wrapped.
|
|
17
|
+
- `batch-find-exercises` and `find-exercise` prefer an exact title match and rank partial matches by shortest title, so "Squat" no longer resolves to an arbitrary variant.
|
|
18
|
+
- RPE inputs validated against Hevy's allowed values.
|
|
19
|
+
- `get-exercise-progression` trends bodyweight exercises on max reps (was always "plateau") and reports a true all-time best; `progressionKg` is now `progression` with a `trendMetric` field.
|
|
20
|
+
- `analyze-workout` judges "reps hit" on the weakest working set, not the mean; `get-training-summary` counts sessions per workout, not per exercise entry.
|
|
21
|
+
- `find-exercise` matches muscle groups written with spaces ("upper back").
|
|
22
|
+
- `create-exercise-template` invalidates the exercise cache so the new exercise is immediately findable.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Built on `@furkantanyol/hevy-client`; axios removed.
|
|
27
|
+
- Coaching prompts moved to `prompts/` (`COACH.md`, `CHATGPT.md`, `GEMINI.md`).
|
|
28
|
+
|
|
29
|
+
## 0.2.0 (2026-05-31)
|
|
30
|
+
|
|
31
|
+
- Body measurement tools. Fixed exercise-history path, custom exercise body fields, routine folder handling.
|
|
32
|
+
|
|
33
|
+
## 0.1.x
|
|
34
|
+
|
|
35
|
+
- Initial MCP server with workout, routine, exercise and coaching tools.
|
package/README.md
CHANGED
|
@@ -1,30 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hevy-coach
|
|
2
2
|
|
|
3
|
-
AI
|
|
3
|
+
MCP server that turns an AI assistant into a strength coach with access to your [Hevy](https://hevyapp.com) account. Every read endpoint, routine and workout writes, and coaching tools built on top: workout-vs-plan analysis, exercise progression with estimated 1RM, training summaries, exercise name resolution.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Requires Node 22.18+ and a Hevy API key (Hevy Settings → API).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- **Coaching intelligence** — workout analysis, progression tracking, automatic adaptation recommendations
|
|
11
|
-
- **Auto-pushed to Hevy** — routines appear in your app, ready to log
|
|
12
|
-
- **Daily briefings** — morning message with today's workout + conditioning + mobility
|
|
13
|
-
- **Adaptive coaching** — reads your completed workouts and adjusts programming
|
|
14
|
-
- **Weekly reviews** — volume tracking, PR highlights, conditioning adherence, program adjustments
|
|
15
|
-
|
|
16
|
-
## Setup
|
|
17
|
-
|
|
18
|
-
### Prerequisites
|
|
19
|
-
|
|
20
|
-
- [Hevy Pro](https://hevyapp.com) subscription (required for API access)
|
|
21
|
-
- API key from Hevy Settings → API
|
|
22
|
-
- Node.js 18+
|
|
23
|
-
- [Claude Desktop](https://claude.ai/download) (or any MCP-compatible client)
|
|
24
|
-
|
|
25
|
-
### Quick Start (npx)
|
|
26
|
-
|
|
27
|
-
Add to your Claude Desktop MCP config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
9
|
+
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
28
10
|
|
|
29
11
|
```json
|
|
30
12
|
{
|
|
@@ -32,145 +14,59 @@ Add to your Claude Desktop MCP config (`~/Library/Application Support/Claude/cla
|
|
|
32
14
|
"hevy-coach": {
|
|
33
15
|
"command": "npx",
|
|
34
16
|
"args": ["-y", "hevy-coach"],
|
|
35
|
-
"env": {
|
|
36
|
-
"HEVY_API_KEY": "your-hevy-api-key"
|
|
37
|
-
}
|
|
17
|
+
"env": { "HEVY_API_KEY": "your-hevy-api-key" }
|
|
38
18
|
}
|
|
39
19
|
}
|
|
40
20
|
}
|
|
41
21
|
```
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
### Install from Source
|
|
23
|
+
**Claude Code:**
|
|
46
24
|
|
|
47
25
|
```bash
|
|
48
|
-
|
|
49
|
-
cd hevy-coach
|
|
50
|
-
npm install
|
|
51
|
-
npm run build
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Then use the local path in your MCP config:
|
|
55
|
-
|
|
56
|
-
```json
|
|
57
|
-
{
|
|
58
|
-
"mcpServers": {
|
|
59
|
-
"hevy-coach": {
|
|
60
|
-
"command": "node",
|
|
61
|
-
"args": ["/absolute/path/to/hevy-coach/dist/cli.js"],
|
|
62
|
-
"env": {
|
|
63
|
-
"HEVY_API_KEY": "your-hevy-api-key"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
26
|
+
claude mcp add hevy-coach -e HEVY_API_KEY=your-hevy-api-key -- npx -y hevy-coach
|
|
68
27
|
```
|
|
69
28
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Create a Claude Project called **"HevyCoach"**:
|
|
73
|
-
|
|
74
|
-
1. Open Claude Desktop → Projects → New Project
|
|
75
|
-
2. Name it "HevyCoach"
|
|
76
|
-
3. In Project Instructions, paste the contents of `CLAUDE.md`
|
|
77
|
-
4. Add `training-history.md` as a project file
|
|
78
|
-
|
|
79
|
-
### Start Training
|
|
80
|
-
|
|
81
|
-
Open the HevyCoach project and say:
|
|
82
|
-
|
|
83
|
-
> "I want to start training with you. Let's do the onboarding."
|
|
84
|
-
|
|
85
|
-
Claude will ask about your body, training history, goals, and preferences. Then it generates your full program (strength + conditioning + mobility) and pushes routines to Hevy.
|
|
86
|
-
|
|
87
|
-
### Daily Automation (Optional)
|
|
88
|
-
|
|
89
|
-
Create two Claude Desktop scheduled tasks:
|
|
90
|
-
|
|
91
|
-
**Morning briefing (7 AM):**
|
|
92
|
-
> Read my HevyCoach program. Tell me today's workout with warmup, exercises, sets/reps/weights, conditioning if scheduled, and daily mobility drills.
|
|
93
|
-
|
|
94
|
-
**Evening sync (11:59 PM):**
|
|
95
|
-
> Fetch my latest Hevy workout. Analyze it against today's plan. Summarize performance, note progressions or missed targets, and adjust upcoming sessions if needed.
|
|
29
|
+
Then add the coaching persona from the repository's `prompts/COACH.md` to your project instructions and say "let's do the onboarding".
|
|
96
30
|
|
|
97
31
|
## Tools
|
|
98
32
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
| `get-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
|
106
|
-
|
|
107
|
-
| `get-
|
|
108
|
-
| `get-
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `get-
|
|
112
|
-
| `
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
|
121
|
-
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
128
|
-
| `get-exercise-templates` | Browse exercise library |
|
|
129
|
-
| `get-exercise-template` | Get exercise details |
|
|
130
|
-
| `get-exercise-history` | Past performance data |
|
|
131
|
-
| `create-exercise-template` | Create custom exercise |
|
|
132
|
-
|
|
133
|
-
### Coaching Tools
|
|
134
|
-
| Tool | Description |
|
|
135
|
-
|------|-------------|
|
|
136
|
-
| `analyze-workout` | Compare completed vs planned workout. Returns per-exercise analysis with progression recommendations. |
|
|
137
|
-
| `get-training-summary` | Aggregate last N workouts: volume, frequency, consistency, duration trends. |
|
|
138
|
-
| `get-exercise-progression` | Track exercise over time: weight/rep trends, estimated 1RM (Epley), plateau detection. |
|
|
139
|
-
| `find-exercise` | Search exercises by name or muscle group. Returns compact id + title pairs. |
|
|
140
|
-
| `batch-find-exercises` | Look up multiple exercises at once. Loads library once, returns map of query → match. Use this when creating routines. |
|
|
141
|
-
|
|
142
|
-
## Architecture
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
hevy-coach/
|
|
146
|
-
├── src/
|
|
147
|
-
│ ├── index.ts # Server factory
|
|
148
|
-
│ ├── cli.ts # CLI entry point (stdio transport)
|
|
149
|
-
│ ├── tools/
|
|
150
|
-
│ │ ├── users.ts # User profile
|
|
151
|
-
│ │ ├── workouts.ts # Workout CRUD
|
|
152
|
-
│ │ ├── routines.ts # Routine CRUD
|
|
153
|
-
│ │ ├── exercises.ts # Exercise templates
|
|
154
|
-
│ │ └── coaching.ts # Coaching intelligence (the differentiator)
|
|
155
|
-
│ └── utils/
|
|
156
|
-
│ ├── hevy-client.ts # Hevy API client
|
|
157
|
-
│ └── response.ts # MCP response helpers
|
|
158
|
-
├── CLAUDE.md # Coaching brain + user profile + program
|
|
159
|
-
├── training-history.md # Long-term tracking template
|
|
160
|
-
└── package.json
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Development
|
|
33
|
+
| Tool | Purpose |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `get-user-info` | Profile |
|
|
36
|
+
| `get-workouts`, `get-workout`, `get-workout-count`, `get-workout-events` | Logged workouts and change feed |
|
|
37
|
+
| `create-workout`, `update-workout` | Log or replace a workout |
|
|
38
|
+
| `get-routines`, `get-routine`, `create-routine`, `update-routine` | Routines (templates) |
|
|
39
|
+
| `get-routine-folders`, `get-routine-folder`, `create-routine-folder` | Folders |
|
|
40
|
+
| `get-exercise-templates`, `get-exercise-template`, `create-exercise-template` | Exercise library |
|
|
41
|
+
| `get-exercise-history` | Every set of one exercise |
|
|
42
|
+
| `get-body-measurements`, `get-body-measurement`, `log-body-measurement` | Body measurements |
|
|
43
|
+
| `analyze-workout` | Completed workout vs plan → per-exercise recommendation |
|
|
44
|
+
| `get-training-summary` | Volume, frequency, duration over the last N workouts |
|
|
45
|
+
| `get-exercise-progression` | Session-by-session weight, reps, Epley 1RM, trend |
|
|
46
|
+
| `find-exercise`, `batch-find-exercises` | Resolve names to template ids |
|
|
47
|
+
|
|
48
|
+
Errors come back with Hevy's HTTP status, error code and message so the assistant can react (e.g. `routine-limit-exceeded` on free accounts).
|
|
49
|
+
|
|
50
|
+
## HTTP transport
|
|
51
|
+
|
|
52
|
+
For assistants that connect over HTTP (ChatGPT, Gemini), run `hevy-coach-http`. It proxies your Hevy account, so it is locked down by default:
|
|
53
|
+
|
|
54
|
+
| Variable | Required | Meaning |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `HEVY_API_KEY` | yes | Hevy API key |
|
|
57
|
+
| `MCP_AUTH_TOKEN` | yes | Bearer token clients must send. Use a long random secret. |
|
|
58
|
+
| `PORT` | no | Default 3000 |
|
|
59
|
+
| `HOST` | no | Default `127.0.0.1`. Anything else requires `MCP_ALLOWED_HOSTS`. |
|
|
60
|
+
| `MCP_ALLOWED_HOSTS` | when exposed | Comma-separated hostnames accepted in `Host` (DNS-rebinding protection) |
|
|
61
|
+
| `MCP_ALLOWED_ORIGINS` | no | Comma-separated browser origins allowed by CORS. Off otherwise. |
|
|
164
62
|
|
|
165
63
|
```bash
|
|
166
|
-
|
|
167
|
-
|
|
64
|
+
HEVY_API_KEY=... MCP_AUTH_TOKEN=$(openssl rand -hex 32) npx -p hevy-coach hevy-coach-http
|
|
65
|
+
# MCP endpoint: http://127.0.0.1:3000/mcp Health: /health
|
|
168
66
|
```
|
|
169
67
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Inspired by [chrisdoc/hevy-mcp](https://github.com/chrisdoc/hevy-mcp) and [tomtorggler/hevy-mcp-server](https://github.com/tomtorggler/hevy-mcp-server). Built from scratch with coaching intelligence as the core focus.
|
|
68
|
+
Put it behind HTTPS (a tunnel or reverse proxy) before pointing a hosted assistant at it.
|
|
173
69
|
|
|
174
70
|
## License
|
|
175
71
|
|
|
176
|
-
MIT
|
|
72
|
+
MIT
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as createServer } from "./src-BxQx3e_u.mjs";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
//#region src/cli.ts
|
|
5
|
+
const apiKey = process.env.HEVY_API_KEY;
|
|
6
|
+
if (!apiKey) {
|
|
7
|
+
console.error("HEVY_API_KEY environment variable is required.");
|
|
8
|
+
console.error("Get your API key from Hevy Settings → API.");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
createServer({ apiKey }).connect(new StdioServerTransport()).catch((error) => {
|
|
12
|
+
console.error("Failed to start hevy-coach:", error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
15
|
+
//#endregion
|
|
16
|
+
export {};
|
package/dist/http.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as createServer } from "./src-BxQx3e_u.mjs";
|
|
3
|
+
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
//#region src/utils/auth.ts
|
|
8
|
+
/** Constant-time bearer check. An empty or missing header never matches. */
|
|
9
|
+
function isAuthorized(authorizationHeader, token) {
|
|
10
|
+
const presented = Buffer.from((authorizationHeader ?? "").replace(/^Bearer\s+/i, ""));
|
|
11
|
+
const expected = Buffer.from(token);
|
|
12
|
+
if (presented.length === 0 || presented.length !== expected.length) return false;
|
|
13
|
+
return timingSafeEqual(presented, expected);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/http.ts
|
|
17
|
+
const log = (message) => console.error(`[hevy-coach] ${message}`);
|
|
18
|
+
const fail = (message) => {
|
|
19
|
+
log(message);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_PORT = 3e3;
|
|
23
|
+
const LOCALHOST = "127.0.0.1";
|
|
24
|
+
const LOCALHOST_ALIASES = /* @__PURE__ */ new Set([
|
|
25
|
+
LOCALHOST,
|
|
26
|
+
"localhost",
|
|
27
|
+
"::1"
|
|
28
|
+
]);
|
|
29
|
+
const csv = (value) => (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
30
|
+
const apiKey = process.env.HEVY_API_KEY || fail("HEVY_API_KEY environment variable is required.");
|
|
31
|
+
const authToken = process.env.MCP_AUTH_TOKEN || fail("MCP_AUTH_TOKEN is required: this server proxies your Hevy account. Generate a long random secret.");
|
|
32
|
+
if (authToken.length < 32) fail(`MCP_AUTH_TOKEN must be at least 32 characters, e.g. openssl rand -hex 32.`);
|
|
33
|
+
const port = Number(process.env.PORT ?? DEFAULT_PORT);
|
|
34
|
+
if (!Number.isInteger(port) || port <= 0) fail(`PORT must be a positive integer, got "${process.env.PORT}".`);
|
|
35
|
+
const host = process.env.HOST ?? LOCALHOST;
|
|
36
|
+
const allowedHosts = csv(process.env.MCP_ALLOWED_HOSTS);
|
|
37
|
+
const allowedOrigins = csv(process.env.MCP_ALLOWED_ORIGINS);
|
|
38
|
+
const isLocal = LOCALHOST_ALIASES.has(host);
|
|
39
|
+
if (!isLocal && allowedHosts.length === 0) fail(`HOST=${host} exposes the server beyond localhost; set MCP_ALLOWED_HOSTS to your public hostname(s).`);
|
|
40
|
+
const app = createMcpExpressApp(isLocal ? { host } : {
|
|
41
|
+
host,
|
|
42
|
+
allowedHosts
|
|
43
|
+
});
|
|
44
|
+
app.use((req, res, next) => {
|
|
45
|
+
const origin = req.headers.origin;
|
|
46
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
47
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
48
|
+
res.setHeader("Vary", "Origin");
|
|
49
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, mcp-session-id");
|
|
50
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
51
|
+
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
52
|
+
}
|
|
53
|
+
if (req.method === "OPTIONS") {
|
|
54
|
+
res.sendStatus(204);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
next();
|
|
58
|
+
});
|
|
59
|
+
function bearerAuth(req, res, next) {
|
|
60
|
+
if (isAuthorized(req.headers.authorization, authToken)) {
|
|
61
|
+
next();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
65
|
+
}
|
|
66
|
+
const transports = /* @__PURE__ */ new Map();
|
|
67
|
+
const jsonRpcError = (res, status, message) => res.status(status).json({
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
error: {
|
|
70
|
+
code: -32e3,
|
|
71
|
+
message
|
|
72
|
+
},
|
|
73
|
+
id: null
|
|
74
|
+
});
|
|
75
|
+
function existingTransport(req, res) {
|
|
76
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
77
|
+
const transport = typeof sessionId === "string" ? transports.get(sessionId) : void 0;
|
|
78
|
+
if (!transport) jsonRpcError(res, 400, "Missing or unknown mcp-session-id");
|
|
79
|
+
return transport;
|
|
80
|
+
}
|
|
81
|
+
async function startSession(req, res) {
|
|
82
|
+
const transport = new StreamableHTTPServerTransport({
|
|
83
|
+
sessionIdGenerator: () => randomUUID(),
|
|
84
|
+
onsessioninitialized: (sid) => {
|
|
85
|
+
transports.set(sid, transport);
|
|
86
|
+
log(`session started ${sid}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
transport.onclose = () => {
|
|
90
|
+
if (transport.sessionId) {
|
|
91
|
+
transports.delete(transport.sessionId);
|
|
92
|
+
log(`session closed ${transport.sessionId}`);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await createServer({ apiKey }).connect(transport);
|
|
96
|
+
await transport.handleRequest(req, res, req.body);
|
|
97
|
+
}
|
|
98
|
+
app.get("/health", (_req, res) => {
|
|
99
|
+
res.json({
|
|
100
|
+
status: "ok",
|
|
101
|
+
server: "hevy-coach"
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
app.post("/mcp", bearerAuth, async (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
if (req.headers["mcp-session-id"]) await existingTransport(req, res)?.handleRequest(req, res, req.body);
|
|
107
|
+
else if (isInitializeRequest(req.body)) await startSession(req, res);
|
|
108
|
+
else jsonRpcError(res, 400, "Bad Request: missing session ID or not an initialize request");
|
|
109
|
+
} catch (error) {
|
|
110
|
+
log(`POST /mcp failed: ${String(error)}`);
|
|
111
|
+
if (!res.headersSent) jsonRpcError(res, 500, "Internal server error");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
for (const method of ["get", "delete"]) app[method]("/mcp", bearerAuth, async (req, res) => {
|
|
115
|
+
const transport = existingTransport(req, res);
|
|
116
|
+
if (!transport) return;
|
|
117
|
+
try {
|
|
118
|
+
await transport.handleRequest(req, res);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
log(`${method.toUpperCase()} /mcp failed: ${String(error)}`);
|
|
121
|
+
if (!res.headersSent) res.status(500).send("Internal server error");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
app.listen(port, host, () => {
|
|
125
|
+
log(`listening on http://${host}:${port}/mcp (bearer auth on)`);
|
|
126
|
+
}).on("error", (error) => fail(`could not listen on ${host}:${port}: ${error.message}`));
|
|
127
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, async () => {
|
|
128
|
+
log("shutting down");
|
|
129
|
+
await Promise.all([...transports.values()].map((t) => t.close().catch(() => void 0)));
|
|
130
|
+
process.exit(0);
|
|
131
|
+
});
|
|
132
|
+
//#endregion
|
|
133
|
+
export {};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { HevyClientOptions } from "@furkantanyol/hevy-client";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
export declare const SERVER_VERSION = "1.0.0";
|
|
5
|
+
export declare function createServer(options: HevyClientOptions): McpServer;
|
|
6
|
+
//#endregion
|
package/dist/index.mjs
ADDED