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/README.md +169 -19
- package/build/api.js +66 -102
- package/build/index.js +627 -372
- package/build/safe.js +11 -63
- package/build/types.js +1 -7
- package/icon.png +0 -0
- package/manifest.json +11 -6
- package/npm-shrinkwrap.json +1903 -0
- package/package.json +8 -4
- package/server.json +3 -3
package/build/safe.js
CHANGED
|
@@ -1,84 +1,32 @@
|
|
|
1
|
-
// Runner-authored text
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
6
|
-
"description": "Read your own ourpr running history in Claude.",
|
|
7
|
-
"long_description": "Runs, mile splits, watch laps, streams, interval workouts
|
|
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": "
|
|
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
|
}
|