ourpr-mcp-server 0.2.0 → 0.4.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/build/safe.js CHANGED
@@ -1,84 +1,32 @@
1
- // Runner-authored text, on its way into an agent's context.
2
- //
3
- // EVERY TOOL RESULT HERE IS UNTRUSTED CONTENT. A run's name and description
4
- // are free text a person typed, or that arrived from a Strava or Garmin
5
- // import. They travel verbatim into the same token stream as the agent's
6
- // instructions, and a model has no hardware boundary between the two. That is
7
- // the whole of the prompt-injection problem and no library fixes it.
8
- //
9
- // Two defences, and they do different jobs.
10
- //
11
- // 1. NEUTRALISE THE STRUCTURE. A `|` or a newline in a name corrupts the
12
- // markdown table it lands in — an agent then reads columns that shifted,
13
- // which is a wrong answer rather than a refused one. Measured against
14
- // 1,000 of Joey's activities: zero names carry one today, the longest name
15
- // is 86 characters, and 400 runs carry a description up to 362. Nothing
16
- // prevents the next one. This is a correctness fix that happens to close an
17
- // injection surface.
18
- //
19
- // 2. NAME THE BOUNDARY. Wrapping the data in explicit delimiters and saying
20
- // once that what is inside is data does not solve injection. It measurably
21
- // reduces how often it lands, which is the honest claim for it, and it
22
- // costs one line per response rather than a preamble on every field.
23
- //
24
- // What this deliberately does NOT do is pattern-match for "ignore previous
25
- // instructions" and friends. That is whack-a-mole against an attacker who
26
- // writes one sentence differently, and it would give a false sense that the
27
- // content had been made safe.
28
- /** The longest a name may be before it is cut. Long enough for every real one
29
- * measured (86), short enough that a pasted paragraph cannot flood a row. */
1
+ // Runner-authored text is untrusted; it is neutralised, never pattern-matched.
2
+ // Longer than any measured name, shorter than a pasted paragraph.
30
3
  const MAX_NAME = 90;
31
- /** A description is prose and belongs on a single run, never in a table. */
4
+ // A description belongs on one run, never in a table.
32
5
  const MAX_NOTE = 300;
33
- /**
34
- * One field of runner-authored text, made safe to put in a table cell.
35
- *
36
- * Control characters and newlines become spaces, a pipe is escaped so the
37
- * column it sits in survives, and the whole thing is capped.
38
- */
6
+ /** One field of runner text, made safe for a table cell. */
39
7
  export function cell(value, max = MAX_NAME) {
40
8
  if (value === null || value === undefined || value === "")
41
9
  return "";
42
- // TAKES `unknown`, NOT `string` (2026-08-30). It was typed `string`, and
43
- // `activity_data.device` turned out to be an OBJECT, so `.replace` threw and
44
- // `ourpr_get_run` failed outright for every run — a whole tool lost to one
45
- // wrong field type. A tool that returns nothing for one field is a small
46
- // fault; a tool that throws is a total one, and this layer sits between the
47
- // agent and data whose shape the server does not control.
48
- //
49
- // A primitive is printed. Anything else returns "" rather than the string
50
- // "[object Object]": rendering junk into an agent's context is worse than
51
- // rendering nothing, and nothing is what the caller already handles.
10
+ // A field can be any JSON value; only a primitive is printed.
52
11
  if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
53
12
  return "";
54
13
  }
55
14
  const flat = String(value)
56
- // Every control character, newline and tab included. A newline in a cell
57
- // ends the row early and every column after it shifts.
15
+ // A newline in a cell ends the row early.
58
16
  .replace(/[\u0000-\u001F\u007F]/g, " ")
59
- // Zero-width and bidirectional marks: invisible in a transcript, and they
60
- // are how text can read one way to a person and another to a parser.
17
+ // Zero-width and bidirectional marks hide text from a reader.
61
18
  .replace(/[\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF\u2060\u00AD]/g, "")
62
- // The tag block, U+E0000-E007F: invisible characters that mirror ASCII,
63
- // which is the known channel for smuggling hidden instructions.
19
+ // The tag block mirrors ASCII invisibly.
64
20
  .replace(/[\u{E0000}-\u{E007F}]/gu, "")
65
21
  .replace(/\s+/g, " ")
66
22
  .trim();
67
23
  const clipped = flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
68
- // Escape rather than delete: a runner who named a run "5k | tempo" should
69
- // still read it back as they wrote it.
24
+ // Escaped, not deleted, so "5k | tempo" reads back as written.
70
25
  return clipped.replace(/\|/g, "\\|");
71
26
  }
72
- /** Prose from one run — a description or a note. Same cleaning, longer cap. */
27
+ /** Prose from one run, with the longer cap. */
73
28
  export const note = (value) => cell(value, MAX_NOTE);
74
- /**
75
- * Mark a block as the runner's own data rather than instructions.
76
- *
77
- * ONE LINE, NOT A PREAMBLE. It rides on every response that carries
78
- * runner-authored text, so its cost is paid on every call and its length is
79
- * part of its design. It is a reduction in how often injection lands, never a
80
- * guarantee, and the code must not be written as though it were one.
81
- */
29
+ /** Marks a block as data; it reduces injection and does not prevent it. */
82
30
  export function fenced(body) {
83
31
  return ("<ourpr-data>\n" +
84
32
  body +
package/build/types.js CHANGED
@@ -1,8 +1,2 @@
1
- // What the ourpr API returns, narrowed to the fields these tools read.
2
- //
3
- // DELIBERATELY PARTIAL. `DetailedActivity` on the backend carries far more
4
- // than this, and copying it whole would put every field into an agent's
5
- // context whether or not a tool uses it. These interfaces name what is read
6
- // and nothing else, which is the same whitelist discipline the backend's own
7
- // response model keeps.
1
+ // Deliberately partial: only the fields a tool reads.
8
2
  export {};
package/icon.png ADDED
Binary file
package/manifest.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "ourpr-mcp-server",
4
- "display_name": "ourpr",
5
- "version": "0.2.0",
6
- "description": "Read your own ourpr running history in Claude.",
7
- "long_description": "Runs, mile splits, watch laps, streams, interval workouts and similar terrain, from your own ourpr account. Read only. The token comes from ourpr Settings and you can revoke it there at any time.",
4
+ "display_name": "ourpr.",
5
+ "version": "0.4.0",
6
+ "description": "Read your own ourpr running history in Claude, and plan the week ahead.",
7
+ "long_description": "Runs, mile splits, watch laps, streams, interval workouts, similar terrain, your goal race and Blocks, your races and your plans, from your own ourpr account, plus one write: a plan on the week ahead, for a token made with the write scope. The token comes from ourpr Settings and you can revoke it there at any time.",
8
8
  "author": { "name": "Joey Flores", "url": "https://ourpr.app" },
9
9
  "homepage": "https://ourpr.app/your-runs",
10
+ "icon": "icon.png",
10
11
  "documentation": "https://github.com/joeyaflores/ourpr-mcp-server#readme",
11
12
  "support": "https://ourpr.app/support",
12
13
  "license": "MIT",
@@ -25,7 +26,7 @@
25
26
  "token": {
26
27
  "type": "string",
27
28
  "title": "Access token",
28
- "description": "From ourpr: Settings, Access tokens, New token.",
29
+ "description": "In ourpr: Profile, Settings, ourpr. mcp, New token.",
29
30
  "sensitive": true,
30
31
  "required": true
31
32
  }
@@ -37,7 +38,11 @@
37
38
  { "name": "ourpr_run_laps", "description": "The laps the watch recorded." },
38
39
  { "name": "ourpr_rep_workouts", "description": "Every interval workout in your history." },
39
40
  { "name": "ourpr_detect_reps", "description": "Whether one run was an interval workout." },
40
- { "name": "ourpr_similar_terrain", "description": "Runs that match a distance and a climb." }
41
+ { "name": "ourpr_similar_terrain", "description": "Runs that match a distance and a climb." },
42
+ { "name": "ourpr_training_blocks", "description": "The goal race, this week of its Block, and past Blocks." },
43
+ { "name": "ourpr_list_races", "description": "Every race in your history, with the fastest at each distance." },
44
+ { "name": "ourpr_list_plans", "description": "The plans already on your week." },
45
+ { "name": "ourpr_plan_week", "description": "Put a plan on a day still ahead. The one write." }
41
46
  ],
42
47
  "compatibility": { "claude_desktop": ">=0.10.0", "platforms": ["darwin", "win32", "linux"], "runtimes": { "node": ">=22.14.0" } }
43
48
  }