remark-mcp 0.1.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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/client.js +128 -0
- package/dist/config.js +30 -0
- package/dist/index.js +161 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Freddie Wheeler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# remark-mcp
|
|
2
|
+
|
|
3
|
+
**Connect Claude to Remark — push documents in for provocation-driven review, pull them back with their full thinking trail.**
|
|
4
|
+
|
|
5
|
+
`remark-mcp` is a small [MCP](https://modelcontextprotocol.io) server. It lets any Claude Code or Claude Desktop session push a document into [Remark](https://remark.freddie.nyc) — optionally with a *context brief* capturing the session's reasoning — and pull any Remark document back as a single composed markdown artifact, including every remark and its status. Work is born in a Claude session, handed to Remark for deeper thinking, and referenceable from any later session.
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
1. **Create a Bridge token** in Remark → Settings → Bridge tokens (copy it once — it is shown only once).
|
|
10
|
+
2. **Add the server to Claude Code**, passing the token via env:
|
|
11
|
+
```sh
|
|
12
|
+
claude mcp add remark --env REMARK_TOKEN=rmk_your_token_here -- npx -y remark-mcp
|
|
13
|
+
```
|
|
14
|
+
3. **Use it**: in a Claude session, say *"push this document to Remark with a brief covering what we decided"* — you'll get back a link. Later, *"pull my latest Remark doc"* or *"what did Remark say about it?"*
|
|
15
|
+
|
|
16
|
+
That's it. The token lives in your client config, never on the command line or in a log.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
All configuration is via environment variables — never argv, so your token never lands in a process list.
|
|
23
|
+
|
|
24
|
+
| Variable | Required | Default | Notes |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| `REMARK_TOKEN` | Yes | — | A Bridge token from Remark → Settings → Bridge tokens (`rmk_…`). |
|
|
27
|
+
| `REMARK_URL` | No | `https://remark.freddie.nyc` | Point at a different Remark instance if you self-host. |
|
|
28
|
+
|
|
29
|
+
The server fails fast at startup with a clear message if `REMARK_TOKEN` is missing.
|
|
30
|
+
|
|
31
|
+
## Client setup
|
|
32
|
+
|
|
33
|
+
The supported (QA'd) clients are **Claude Code** and **Claude Desktop**. Cursor and VS Code use the same standard MCP config and should work, but are documented-not-guaranteed.
|
|
34
|
+
|
|
35
|
+
### Claude Code
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
claude mcp add remark --env REMARK_TOKEN=rmk_your_token_here -- npx -y remark-mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Claude Desktop
|
|
42
|
+
|
|
43
|
+
**Option A — one-click bundle (`.mcpb`):** download the `remark-mcp.mcpb` file from the [latest release](https://github.com/freddie-wheeler/remark-mcp/releases), double-click it to install into Claude Desktop, and paste your token when prompted. No terminal required.
|
|
44
|
+
|
|
45
|
+
**Option B — config file:** add this to `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"remark": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "remark-mcp"],
|
|
53
|
+
"env": { "REMARK_TOKEN": "rmk_your_token_here" }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Restart Claude Desktop after editing.
|
|
60
|
+
|
|
61
|
+
### Cursor
|
|
62
|
+
|
|
63
|
+
Add to `~/.cursor/mcp.json` (or the project `.cursor/mcp.json`):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"remark": {
|
|
69
|
+
"command": "npx",
|
|
70
|
+
"args": ["-y", "remark-mcp"],
|
|
71
|
+
"env": { "REMARK_TOKEN": "rmk_your_token_here" }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### VS Code
|
|
78
|
+
|
|
79
|
+
Add to your user or workspace MCP config:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"servers": {
|
|
84
|
+
"remark": {
|
|
85
|
+
"command": "npx",
|
|
86
|
+
"args": ["-y", "remark-mcp"],
|
|
87
|
+
"env": { "REMARK_TOKEN": "rmk_your_token_here" }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Run from source (development / pre-release)
|
|
94
|
+
|
|
95
|
+
Before the npm package is published you can run the server straight from a clone. Requires [Bun](https://bun.sh).
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
git clone https://github.com/freddie-wheeler/remark-mcp.git
|
|
99
|
+
cd remark-mcp
|
|
100
|
+
bun install
|
|
101
|
+
# then point your client at the local entry:
|
|
102
|
+
claude mcp add remark --env REMARK_TOKEN=rmk_your_token_here -- bun run /absolute/path/to/remark-mcp/src/index.ts
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Creating a Bridge token
|
|
106
|
+
|
|
107
|
+
1. Open Remark and go to **Settings → Bridge tokens**.
|
|
108
|
+
2. Click **Create token**, give it a name (e.g. "Claude Code"), and confirm.
|
|
109
|
+
3. **Copy the `rmk_…` value immediately** — it is shown exactly once and cannot be retrieved later. If you lose it, revoke it and create another.
|
|
110
|
+
4. Provide it to your MCP client as `REMARK_TOKEN` (see the config blocks above).
|
|
111
|
+
|
|
112
|
+
To rotate: create a new token, update `REMARK_TOKEN`, reconnect the server, then revoke the old one.
|
|
113
|
+
|
|
114
|
+

|
|
115
|
+
|
|
116
|
+
## Tools
|
|
117
|
+
|
|
118
|
+
### `remark_push`
|
|
119
|
+
|
|
120
|
+
Push a document into Remark for review. Triggers on *"push / send / transfer this to Remark"*.
|
|
121
|
+
|
|
122
|
+
| Arg | Required | Description |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `markdown` | Yes | The full document as markdown. |
|
|
125
|
+
| `title` | No | Defaults to the first heading or line. |
|
|
126
|
+
| `goal` | No | What you want from the review (e.g. "find the weak assumptions"). |
|
|
127
|
+
| `context_brief` | No (recommended) | A markdown summary of the session: decisions, rejected paths, open questions. Stored with the document. |
|
|
128
|
+
|
|
129
|
+
> *"Push this strategy memo to Remark with a brief covering what we decided and what we ruled out."*
|
|
130
|
+
|
|
131
|
+
### `remark_list`
|
|
132
|
+
|
|
133
|
+
List your Remark documents, newest first (id, title, goal, lens, updated-at). No arguments.
|
|
134
|
+
|
|
135
|
+
> *"What documents do I have in Remark?"*
|
|
136
|
+
|
|
137
|
+
### `remark_get`
|
|
138
|
+
|
|
139
|
+
Pull a document back as a composed artifact — header, context briefs, content, and the full provocation trail with statuses and replies. Triggers on *"get / pull / reference my Remark doc"* and *"what did Remark say about…"*.
|
|
140
|
+
|
|
141
|
+
| Arg | Required | Description |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| `document_id` | Yes | The document to fetch (use `remark_list` to find it). |
|
|
144
|
+
| `format` | No | `markdown` (default) or `json`. |
|
|
145
|
+
|
|
146
|
+
> *"Pull document abc123 from Remark and tell me what was dismissed and why."*
|
|
147
|
+
|
|
148
|
+
## Security & privacy
|
|
149
|
+
|
|
150
|
+
- **No telemetry.** This client sends nothing anywhere except your own Remark instance. It holds no state, writes no files, and keeps no logs of its own.
|
|
151
|
+
- **Token via env only.** `REMARK_TOKEN` is read from the environment, never accepted on the command line (which would leak it into process lists), and never written to any log.
|
|
152
|
+
- **Content stays between you and Remark.** Document content and context briefs flow only between your machine and your own Remark account. They are never logged by this server.
|
|
153
|
+
- **Consent.** Whether your sessions contribute to Remark's learning is governed by your Remark account's consent setting, not by this client.
|
|
154
|
+
|
|
155
|
+
## Troubleshooting
|
|
156
|
+
|
|
157
|
+
- **"token invalid or revoked (401)"** — the token is wrong, expired, or revoked. Create a fresh one in Remark → Settings → Bridge tokens and update `REMARK_TOKEN`, then reconnect the server.
|
|
158
|
+
- **"could not reach Remark at …"** — check `REMARK_URL` (default `https://remark.freddie.nyc`) and your network. The error names the exact URL tried.
|
|
159
|
+
- **"document not found (404)"** — the `document_id` is wrong or belongs to another account. Run `remark_list` to get valid ids.
|
|
160
|
+
- **`npx` / Node not found** — the `npx` route needs Node 18+ on your PATH. Either install Node, or use the *Run from source* route above with Bun.
|
|
161
|
+
- **Server won't start** — if `REMARK_TOKEN` is unset the server exits immediately with a message pointing you to Settings. Confirm your client is passing the `env` block.
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
bun install
|
|
167
|
+
bun run typecheck # tsc --noEmit
|
|
168
|
+
bun run build # emits dist/
|
|
169
|
+
bun run start # run the server on stdio
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
[MIT](LICENSE)
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// A failure whose `message` is safe to show the user as-is. Thrown for every
|
|
2
|
+
// mapped error condition so the server layer never has to interpret raw
|
|
3
|
+
// fetch/HTTP internals.
|
|
4
|
+
export class RemarkError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export class RemarkClient {
|
|
7
|
+
token;
|
|
8
|
+
baseUrl;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.token = config.token;
|
|
11
|
+
this.baseUrl = config.baseUrl;
|
|
12
|
+
}
|
|
13
|
+
// POST /api/bridge/documents — create, or update in place when document_id
|
|
14
|
+
// is supplied.
|
|
15
|
+
async push(input) {
|
|
16
|
+
const body = { markdown: input.markdown };
|
|
17
|
+
if (input.title)
|
|
18
|
+
body.title = input.title;
|
|
19
|
+
if (input.goal)
|
|
20
|
+
body.goal = input.goal;
|
|
21
|
+
if (input.lens_doc_type)
|
|
22
|
+
body.lens_doc_type = input.lens_doc_type;
|
|
23
|
+
if (input.lens_stage)
|
|
24
|
+
body.lens_stage = input.lens_stage;
|
|
25
|
+
if (input.context_brief)
|
|
26
|
+
body.context_brief = input.context_brief;
|
|
27
|
+
if (input.document_id)
|
|
28
|
+
body.document_id = input.document_id;
|
|
29
|
+
const res = await this.fetch('/api/bridge/documents', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw await this.mapError(res, { documentId: input.document_id });
|
|
36
|
+
}
|
|
37
|
+
const data = (await this.json(res));
|
|
38
|
+
if (!data.id || !data.url) {
|
|
39
|
+
throw new RemarkError(`Remark returned an unexpected response to the push (HTTP ${res.status}).`);
|
|
40
|
+
}
|
|
41
|
+
return { id: data.id, url: data.url };
|
|
42
|
+
}
|
|
43
|
+
// GET /api/bridge/documents — the caller's documents, newest first.
|
|
44
|
+
async list() {
|
|
45
|
+
const res = await this.fetch('/api/bridge/documents', { method: 'GET' });
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
throw await this.mapError(res, {});
|
|
48
|
+
}
|
|
49
|
+
const data = (await this.json(res));
|
|
50
|
+
return data.documents ?? [];
|
|
51
|
+
}
|
|
52
|
+
// GET /api/bridge/documents/[id] — the composed read. `format: 'json'`
|
|
53
|
+
// returns the structured shape; markdown (default) returns the composed
|
|
54
|
+
// artifact as a single string.
|
|
55
|
+
async get(documentId, format) {
|
|
56
|
+
const suffix = format === 'json' ? '?format=json' : '';
|
|
57
|
+
const path = `/api/bridge/documents/${encodeURIComponent(documentId)}${suffix}`;
|
|
58
|
+
const res = await this.fetch(path, { method: 'GET' });
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw await this.mapError(res, { documentId });
|
|
61
|
+
}
|
|
62
|
+
if (format === 'json') {
|
|
63
|
+
return { format: 'json', body: await this.json(res) };
|
|
64
|
+
}
|
|
65
|
+
return { format: 'markdown', body: await res.text() };
|
|
66
|
+
}
|
|
67
|
+
// --- internals ----------------------------------------------------------
|
|
68
|
+
async fetch(path, init) {
|
|
69
|
+
const url = `${this.baseUrl}${path}`;
|
|
70
|
+
try {
|
|
71
|
+
return await fetch(url, {
|
|
72
|
+
...init,
|
|
73
|
+
headers: {
|
|
74
|
+
...(init.headers ?? {}),
|
|
75
|
+
Authorization: `Bearer ${this.token}`,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Network-layer failure (DNS, connection refused, TLS). Name the URL
|
|
81
|
+
// tried so the user can spot a wrong REMARK_URL or an offline host.
|
|
82
|
+
throw new RemarkError(`Could not reach Remark at ${this.baseUrl}. Check REMARK_URL and your ` +
|
|
83
|
+
`network connection. (Tried ${init.method ?? 'GET'} ${url}.)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async json(res) {
|
|
87
|
+
try {
|
|
88
|
+
return await res.json();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new RemarkError(`Remark returned a response that could not be parsed as JSON (HTTP ${res.status}).`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Maps an HTTP error status to a user-facing message. 401 and 404 are
|
|
95
|
+
// product surfaces with specific guidance; everything else is reported with
|
|
96
|
+
// the status and any server-provided message, but never a stack trace.
|
|
97
|
+
async mapError(res, ctx) {
|
|
98
|
+
if (res.status === 401) {
|
|
99
|
+
return new RemarkError('Remark rejected the token (401): it is invalid or has been revoked. ' +
|
|
100
|
+
'Create a new one in Remark → Settings → Bridge tokens and set it as ' +
|
|
101
|
+
'REMARK_TOKEN.');
|
|
102
|
+
}
|
|
103
|
+
if (res.status === 404) {
|
|
104
|
+
const which = ctx.documentId ? ` "${ctx.documentId}"` : '';
|
|
105
|
+
return new RemarkError(`Remark could not find that document${which} (404): the id does not exist or does not belong to your account.`);
|
|
106
|
+
}
|
|
107
|
+
// Other statuses: surface the server's message when present, else a
|
|
108
|
+
// generic line. Read the body defensively — it may be JSON or text.
|
|
109
|
+
let detail = '';
|
|
110
|
+
try {
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
if (text) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(text);
|
|
115
|
+
detail = parsed.error ?? text;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
detail = text;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// ignore — no readable body
|
|
124
|
+
}
|
|
125
|
+
const suffix = detail ? ` — ${detail}` : '';
|
|
126
|
+
return new RemarkError(`Remark request failed (HTTP ${res.status})${suffix}.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Configuration is env-only. Tokens are NEVER accepted via argv (they would
|
|
2
|
+
// leak into process lists) and NEVER logged.
|
|
3
|
+
export const DEFAULT_REMARK_URL = 'https://remark.freddie.nyc';
|
|
4
|
+
// The eight lens document types the Remark app recognises (mirrors
|
|
5
|
+
// `DOC_TYPES` in the remark repo). `lens_stage` is free text, not an enum.
|
|
6
|
+
export const LENS_DOC_TYPES = [
|
|
7
|
+
'strategy-memo',
|
|
8
|
+
'prd',
|
|
9
|
+
'decision-memo',
|
|
10
|
+
'launch-plan',
|
|
11
|
+
'postmortem',
|
|
12
|
+
'vision-belief',
|
|
13
|
+
'essay',
|
|
14
|
+
'other',
|
|
15
|
+
];
|
|
16
|
+
// Reads and validates configuration from the environment. Throws with a
|
|
17
|
+
// user-facing message (no token echoed) when REMARK_TOKEN is absent so the
|
|
18
|
+
// server fails fast at startup.
|
|
19
|
+
export function loadConfig(env = process.env) {
|
|
20
|
+
const token = env.REMARK_TOKEN?.trim();
|
|
21
|
+
if (!token) {
|
|
22
|
+
throw new Error('REMARK_TOKEN is not set. Create a Bridge token in Remark → Settings → ' +
|
|
23
|
+
'Bridge tokens, then provide it to this server as the REMARK_TOKEN ' +
|
|
24
|
+
'environment variable (never on the command line).');
|
|
25
|
+
}
|
|
26
|
+
const rawUrl = env.REMARK_URL?.trim() || DEFAULT_REMARK_URL;
|
|
27
|
+
// Strip any trailing slash so path joins are predictable.
|
|
28
|
+
const baseUrl = rawUrl.replace(/\/+$/, '');
|
|
29
|
+
return { token, baseUrl };
|
|
30
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { RemarkClient, RemarkError } from './client.js';
|
|
6
|
+
import { LENS_DOC_TYPES, loadConfig } from './config.js';
|
|
7
|
+
// remark-mcp — a stdio MCP server that connects Claude to Remark. It is a thin
|
|
8
|
+
// wrapper over the Remark Bridge API: push a document in (optionally with a
|
|
9
|
+
// context brief carrying the session's reasoning), list your documents, and
|
|
10
|
+
// pull any document back as a composed markdown artifact including its full
|
|
11
|
+
// provocation trail.
|
|
12
|
+
//
|
|
13
|
+
// This process speaks JSON-RPC over stdout — nothing else may be written
|
|
14
|
+
// there. All diagnostics go to stderr via console.error, and never include
|
|
15
|
+
// document content or token material.
|
|
16
|
+
const VERSION = '0.1.0';
|
|
17
|
+
// Wraps a client call so every thrown error reaches the model as a clean,
|
|
18
|
+
// actionable message (isError result) rather than a transport-level failure.
|
|
19
|
+
async function toolResult(fn) {
|
|
20
|
+
try {
|
|
21
|
+
return { content: [{ type: 'text', text: await fn() }] };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const message = err instanceof RemarkError
|
|
25
|
+
? err.message
|
|
26
|
+
: 'Unexpected error talking to Remark. Check REMARK_URL and that the ' +
|
|
27
|
+
'Remark service is reachable.';
|
|
28
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function buildServer(config) {
|
|
32
|
+
const client = new RemarkClient(config);
|
|
33
|
+
const server = new McpServer({ name: 'remark-mcp', version: VERSION });
|
|
34
|
+
// --- remark_push -------------------------------------------------------
|
|
35
|
+
server.registerTool('remark_push', {
|
|
36
|
+
title: 'Push a document to Remark',
|
|
37
|
+
description: 'Push, send, or transfer a document to Remark for provocation-driven ' +
|
|
38
|
+
'review. Use this when the user says to "push/send/transfer this to ' +
|
|
39
|
+
'Remark", or wants a piece of writing (a memo, PRD, strategy doc, ' +
|
|
40
|
+
'essay, proposal) reviewed in Remark. Pass the full document as ' +
|
|
41
|
+
'markdown. Strongly prefer to include a `context_brief`: a short ' +
|
|
42
|
+
'summary of the session that produced the document — what was decided, ' +
|
|
43
|
+
'which paths were rejected and why, and any open questions — so the ' +
|
|
44
|
+
"reviewer inherits the session's reasoning. Supply `document_id` to " +
|
|
45
|
+
'update a document you pushed earlier instead of creating a new one.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
markdown: z
|
|
48
|
+
.string()
|
|
49
|
+
.describe('The full document content as markdown. Required.'),
|
|
50
|
+
title: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Document title. Defaults to the first heading or line if omitted.'),
|
|
54
|
+
goal: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('What the author wants from this document — e.g. "sharpen the ' +
|
|
58
|
+
'argument", "find the weak assumptions". Guides the review.'),
|
|
59
|
+
lens_doc_type: z
|
|
60
|
+
.enum(LENS_DOC_TYPES)
|
|
61
|
+
.optional()
|
|
62
|
+
.describe(`The kind of document, which tunes the review lens. One of: ${LENS_DOC_TYPES.join(', ')}. Omit to let Remark infer it.`),
|
|
63
|
+
lens_stage: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Free-text maturity stage — e.g. "early exploration", ' +
|
|
67
|
+
'"pre-users", "ready to ship". Optional.'),
|
|
68
|
+
context_brief: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('A markdown summary of the session that produced this document: ' +
|
|
72
|
+
'key decisions, rejected alternatives and why, and open ' +
|
|
73
|
+
'questions. Highly recommended — it is stored with the document ' +
|
|
74
|
+
'and gives the review its reasoning trail.'),
|
|
75
|
+
document_id: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('The id of an existing Remark document to update in place. Omit ' +
|
|
79
|
+
'to create a new document.'),
|
|
80
|
+
},
|
|
81
|
+
}, (args) => toolResult(async () => {
|
|
82
|
+
const { id, url } = await client.push(args);
|
|
83
|
+
const verb = args.document_id ? 'Updated' : 'Pushed to';
|
|
84
|
+
return `${verb} Remark. Document id: ${id}\nOpen it: ${url}`;
|
|
85
|
+
}));
|
|
86
|
+
// --- remark_list -------------------------------------------------------
|
|
87
|
+
server.registerTool('remark_list', {
|
|
88
|
+
title: 'List your Remark documents',
|
|
89
|
+
description: "List the documents in the user's Remark account, newest first. Use " +
|
|
90
|
+
"this to find a document's id before pulling it back with remark_get, " +
|
|
91
|
+
'or when the user asks "what\'s in my Remark", "what documents do I ' +
|
|
92
|
+
'have in Remark", or wants to pick one to reference.',
|
|
93
|
+
inputSchema: {},
|
|
94
|
+
}, () => toolResult(async () => {
|
|
95
|
+
const docs = await client.list();
|
|
96
|
+
if (docs.length === 0) {
|
|
97
|
+
return 'No documents in your Remark account yet.';
|
|
98
|
+
}
|
|
99
|
+
const lines = docs.map((d) => {
|
|
100
|
+
const title = d.title?.trim() || 'Untitled';
|
|
101
|
+
const parts = [`- ${title} — id: ${d.id}`];
|
|
102
|
+
if (d.goal?.trim())
|
|
103
|
+
parts.push(` goal: ${d.goal.trim()}`);
|
|
104
|
+
if (d.lens?.trim())
|
|
105
|
+
parts.push(` lens: ${d.lens.trim()}`);
|
|
106
|
+
if (d.updated_at)
|
|
107
|
+
parts.push(` updated: ${d.updated_at}`);
|
|
108
|
+
return parts.join('\n');
|
|
109
|
+
});
|
|
110
|
+
return `${docs.length} document(s):\n${lines.join('\n')}`;
|
|
111
|
+
}));
|
|
112
|
+
// --- remark_get --------------------------------------------------------
|
|
113
|
+
server.registerTool('remark_get', {
|
|
114
|
+
title: 'Get a Remark document with its thinking trail',
|
|
115
|
+
description: 'Get, pull, or reference a Remark document as a composed markdown ' +
|
|
116
|
+
'artifact: its header, any context briefs, the content, and the full ' +
|
|
117
|
+
'provocation trail — every remark with its status (kept, dismissed ' +
|
|
118
|
+
'with reason, suggested, accepted, rejected, resolved) and replies. ' +
|
|
119
|
+
'Use this when the user says "get/pull/reference my Remark doc", or ' +
|
|
120
|
+
'asks "what did Remark say about…", "what did the provocations ' +
|
|
121
|
+
'raise", or "what was dismissed and why". Find the id first with ' +
|
|
122
|
+
'remark_list if you do not have it.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
document_id: z
|
|
125
|
+
.string()
|
|
126
|
+
.describe('The id of the Remark document to fetch. Required.'),
|
|
127
|
+
format: z
|
|
128
|
+
.enum(['markdown', 'json'])
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('Output format. "markdown" (default) returns the composed ' +
|
|
131
|
+
'artifact; "json" returns the same data structured.'),
|
|
132
|
+
},
|
|
133
|
+
}, (args) => toolResult(async () => {
|
|
134
|
+
const format = args.format ?? 'markdown';
|
|
135
|
+
const result = await client.get(args.document_id, format);
|
|
136
|
+
if (result.format === 'json') {
|
|
137
|
+
return JSON.stringify(result.body, null, 2);
|
|
138
|
+
}
|
|
139
|
+
return result.body;
|
|
140
|
+
}));
|
|
141
|
+
return server;
|
|
142
|
+
}
|
|
143
|
+
async function main() {
|
|
144
|
+
let config;
|
|
145
|
+
try {
|
|
146
|
+
config = loadConfig();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
// Fail fast with a clear message on stderr; do not start the transport.
|
|
150
|
+
console.error(`remark-mcp: ${err instanceof Error ? err.message : String(err)}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const server = buildServer(config);
|
|
154
|
+
const transport = new StdioServerTransport();
|
|
155
|
+
await server.connect(transport);
|
|
156
|
+
console.error(`remark-mcp ${VERSION} ready (target ${config.baseUrl}). Waiting for requests on stdio.`);
|
|
157
|
+
}
|
|
158
|
+
main().catch((err) => {
|
|
159
|
+
console.error(`remark-mcp: fatal — ${err instanceof Error ? err.message : String(err)}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "remark-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Connect Claude to Remark — push documents in for provocation-driven review, pull them back with their full thinking trail.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Freddie Wheeler",
|
|
7
|
+
"mcpName": "io.github.freddie-wheeler/remark-mcp",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"homepage": "https://github.com/freddie-wheeler/remark-mcp#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/freddie-wheeler/remark-mcp.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/freddie-wheeler/remark-mcp/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"remark",
|
|
21
|
+
"claude"
|
|
22
|
+
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"remark-mcp": "dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"lint": "biome check src/",
|
|
38
|
+
"format": "biome check --write src/",
|
|
39
|
+
"start": "bun run src/index.ts",
|
|
40
|
+
"dev": "bun run src/index.ts",
|
|
41
|
+
"prepublishOnly": "bun run build"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
45
|
+
"zod": "^3.23.8"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "1.9.4",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"typescript": "^5.5.0"
|
|
51
|
+
}
|
|
52
|
+
}
|