standup-mr 0.4.0 → 0.5.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 +34 -0
- package/README.md +15 -3
- package/dist/{chunk-2X2XEEUY.js → chunk-S2PFIUNI.js} +49 -1
- package/dist/cli.js +10 -33
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -1
- package/dist/mcp/server.js +119 -4
- package/llms-install.md +79 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ All notable changes to standup-mr are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.5.0] - 2026-09-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`post_standup_note`** — posts a finished note to a Slack or Discord
|
|
12
|
+
webhook, so an MCP client can deliver the note it just wrote instead of
|
|
13
|
+
handing it back as text. The webhook URL comes from `STANDUP_WEBHOOK_URL`
|
|
14
|
+
and is never a tool argument: anyone holding that URL can post to the
|
|
15
|
+
channel, which makes it a credential. The payload shape is inferred from the
|
|
16
|
+
URL host, so `kind` is only needed when a proxy hides it. A missing URL, an
|
|
17
|
+
unrecognisable host, or a webhook that rejects the message is reported as an
|
|
18
|
+
error — a note is never announced as sent when it was not — and the error
|
|
19
|
+
text never repeats the URL.
|
|
20
|
+
- **`get_note_instructions`** — returns the note-writing rules the Claude Code
|
|
21
|
+
skill carries, so a non-Claude assistant can write the note the same way
|
|
22
|
+
instead of inventing a format. Same content as `standup instructions`.
|
|
23
|
+
|
|
24
|
+
Both were already in the CLI. Only the MCP server was missing them.
|
|
25
|
+
|
|
26
|
+
## [0.4.1] - 2026-09-02
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `get_standup_data` documents itself properly. The description now gives the
|
|
31
|
+
shape of the object it returns, says the tool is a snapshot rather than a
|
|
32
|
+
search API and what that rules out, and spells out failure behaviour: a
|
|
33
|
+
rejected token, a refused resource or a rate limit surfaces the host's own
|
|
34
|
+
message after two retries on transient errors, and a blocker whose
|
|
35
|
+
diagnosis could not be fetched still comes back with `job: "unknown"`. The
|
|
36
|
+
parameter descriptions gained the interactions the schema cannot express —
|
|
37
|
+
`host` is required for GitLab and optional for GitHub, a recognisable host
|
|
38
|
+
settles `provider` on its own, and `lang` relabels dates without
|
|
39
|
+
translating anything.
|
|
40
|
+
|
|
7
41
|
## [0.4.0] - 2026-09-01
|
|
8
42
|
|
|
9
43
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# standup-mr
|
|
2
2
|
|
|
3
3
|
[](https://glama.ai/mcp/servers/Jubstaaa/standup-mr)
|
|
4
|
+
[](https://mcpservers.org/servers/jubstaaa/standup-mr)
|
|
4
5
|
|
|
5
6
|
Standup notes from **merge request state**, not commit logs.
|
|
6
7
|
|
|
@@ -116,9 +117,20 @@ if a snippet above doesn't work, check [Cursor's MCP
|
|
|
116
117
|
docs](https://docs.cursor.com/context/mcp) or Codex's own config
|
|
117
118
|
documentation for the current format rather than trusting this file blindly.
|
|
118
119
|
|
|
119
|
-
The
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
The server exposes three tools:
|
|
121
|
+
|
|
122
|
+
| Tool | What it does |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `get_standup_data` | Reads the provider and returns the report as JSON. Optional `provider`, `host`, `lang`. |
|
|
125
|
+
| `get_note_instructions` | Returns the note-writing rules, so the assistant can write the note the way the skill would. |
|
|
126
|
+
| `post_standup_note` | Posts a finished note to a Slack or Discord webhook. |
|
|
127
|
+
|
|
128
|
+
`post_standup_note` reads the webhook URL from `STANDUP_WEBHOOK_URL` and never
|
|
129
|
+
takes it as an argument — anyone holding that URL can post to the channel, so
|
|
130
|
+
it belongs with the tokens, not in a transcript. The payload shape is inferred
|
|
131
|
+
from the URL host; `kind` is only needed when a proxy hides it.
|
|
132
|
+
|
|
133
|
+
Outside MCP, the same rules are available on stdout:
|
|
122
134
|
|
|
123
135
|
```bash
|
|
124
136
|
npx standup-mr instructions >> AGENTS.md
|
|
@@ -206,6 +206,20 @@ async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
|
|
|
206
206
|
throw new Error(`Webhook rejected the message: HTTP ${response.status}.`);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
+
var WEBHOOK_HOSTS = {
|
|
210
|
+
"hooks.slack.com": "slack",
|
|
211
|
+
"discord.com": "discord",
|
|
212
|
+
"discordapp.com": "discord",
|
|
213
|
+
"ptb.discord.com": "discord",
|
|
214
|
+
"canary.discord.com": "discord"
|
|
215
|
+
};
|
|
216
|
+
function inferWebhookKind(url) {
|
|
217
|
+
try {
|
|
218
|
+
return WEBHOOK_HOSTS[new URL(url).hostname] ?? null;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
209
223
|
|
|
210
224
|
// src/providers/base/http.ts
|
|
211
225
|
function buildUrl(api, path, params) {
|
|
@@ -1133,6 +1147,37 @@ async function buildReport(provider, today, lang = "en", lookbackDays = LOOKBACK
|
|
|
1133
1147
|
};
|
|
1134
1148
|
}
|
|
1135
1149
|
|
|
1150
|
+
// src/manifest/manifest.ts
|
|
1151
|
+
import { existsSync, readFileSync } from "fs";
|
|
1152
|
+
import { dirname, join } from "path";
|
|
1153
|
+
import { fileURLToPath } from "url";
|
|
1154
|
+
function findPackageRoot(startDir) {
|
|
1155
|
+
let dir = startDir;
|
|
1156
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
1157
|
+
const parent = dirname(dir);
|
|
1158
|
+
if (parent === dir) {
|
|
1159
|
+
throw new Error(`Could not locate package.json above ${startDir}`);
|
|
1160
|
+
}
|
|
1161
|
+
dir = parent;
|
|
1162
|
+
}
|
|
1163
|
+
return dir;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/skill/skill.ts
|
|
1167
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1168
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1169
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1170
|
+
function readStandupSkillBody() {
|
|
1171
|
+
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1172
|
+
const skillPath = join2(findPackageRoot(moduleDir), "skills", "standup", "SKILL.md");
|
|
1173
|
+
if (!existsSync2(skillPath)) {
|
|
1174
|
+
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1175
|
+
}
|
|
1176
|
+
const content = readFileSync2(skillPath, "utf8");
|
|
1177
|
+
const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
1178
|
+
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1136
1181
|
export {
|
|
1137
1182
|
STALE_DAYS,
|
|
1138
1183
|
isoDay,
|
|
@@ -1153,6 +1198,7 @@ export {
|
|
|
1153
1198
|
ghHosts,
|
|
1154
1199
|
ghToken,
|
|
1155
1200
|
postWebhook,
|
|
1201
|
+
inferWebhookKind,
|
|
1156
1202
|
buildUrl,
|
|
1157
1203
|
ApiError,
|
|
1158
1204
|
assertUsable,
|
|
@@ -1175,5 +1221,7 @@ export {
|
|
|
1175
1221
|
chooseKind,
|
|
1176
1222
|
connect,
|
|
1177
1223
|
toMarkdown,
|
|
1178
|
-
buildReport
|
|
1224
|
+
buildReport,
|
|
1225
|
+
findPackageRoot,
|
|
1226
|
+
readStandupSkillBody
|
|
1179
1227
|
};
|
package/dist/cli.js
CHANGED
|
@@ -3,14 +3,16 @@ import {
|
|
|
3
3
|
ConfigError,
|
|
4
4
|
buildReport,
|
|
5
5
|
connect,
|
|
6
|
+
findPackageRoot,
|
|
6
7
|
postWebhook,
|
|
8
|
+
readStandupSkillBody,
|
|
7
9
|
toMarkdown
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-S2PFIUNI.js";
|
|
9
11
|
|
|
10
12
|
// src/cli/cli.ts
|
|
11
|
-
import {
|
|
12
|
-
import { dirname
|
|
13
|
-
import { fileURLToPath
|
|
13
|
+
import { realpathSync } from "fs";
|
|
14
|
+
import { dirname, join } from "path";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
14
16
|
import { parseArgs } from "util";
|
|
15
17
|
|
|
16
18
|
// src/cli/cli.constants.ts
|
|
@@ -30,43 +32,18 @@ Credentials resolve as: flag, then GITHUB_HOST / GITHUB_TOKEN (or GITLAB_HOST /
|
|
|
30
32
|
GITLAB_TOKEN), then the gh or glab config.
|
|
31
33
|
--text defaults to '-', meaning read stdin.
|
|
32
34
|
|
|
33
|
-
standup mcp starts the stdio MCP server, exposing get_standup_data
|
|
35
|
+
standup mcp starts the stdio MCP server, exposing get_standup_data,
|
|
36
|
+
post_standup_note and get_note_instructions.
|
|
34
37
|
standup instructions prints the note-writing playbook (the standup skill body)
|
|
35
38
|
to stdout, for teaching a non-Claude assistant the same rules, e.g.:
|
|
36
39
|
npx standup-mr instructions >> AGENTS.md`;
|
|
37
40
|
|
|
38
|
-
// src/manifest/manifest.ts
|
|
39
|
-
import { existsSync, readFileSync } from "fs";
|
|
40
|
-
import { dirname, join } from "path";
|
|
41
|
-
import { fileURLToPath } from "url";
|
|
42
|
-
function findPackageRoot(startDir) {
|
|
43
|
-
let dir = startDir;
|
|
44
|
-
while (!existsSync(join(dir, "package.json"))) {
|
|
45
|
-
const parent = dirname(dir);
|
|
46
|
-
if (parent === dir) {
|
|
47
|
-
throw new Error(`Could not locate package.json above ${startDir}`);
|
|
48
|
-
}
|
|
49
|
-
dir = parent;
|
|
50
|
-
}
|
|
51
|
-
return dir;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
41
|
// src/cli/cli.ts
|
|
55
42
|
async function readStdin() {
|
|
56
43
|
const chunks = [];
|
|
57
44
|
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
58
45
|
return Buffer.concat(chunks).toString("utf8");
|
|
59
46
|
}
|
|
60
|
-
function readStandupSkillBody() {
|
|
61
|
-
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
62
|
-
const skillPath = join2(findPackageRoot(moduleDir), "skills", "standup", "SKILL.md");
|
|
63
|
-
if (!existsSync2(skillPath)) {
|
|
64
|
-
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
65
|
-
}
|
|
66
|
-
const content = readFileSync2(skillPath, "utf8");
|
|
67
|
-
const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
68
|
-
return withoutFrontmatter.replace(/^\n+/, "");
|
|
69
|
-
}
|
|
70
47
|
async function main(argv) {
|
|
71
48
|
const [command, ...rest] = argv;
|
|
72
49
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -115,8 +92,8 @@ async function main(argv) {
|
|
|
115
92
|
return 0;
|
|
116
93
|
}
|
|
117
94
|
if (command === "mcp") {
|
|
118
|
-
const moduleDir =
|
|
119
|
-
const serverPath =
|
|
95
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
96
|
+
const serverPath = join(findPackageRoot(moduleDir), "dist", "mcp", "server.js");
|
|
120
97
|
const serverUrl = pathToFileURL(serverPath).href;
|
|
121
98
|
const { main: startMcpServer } = await import(serverUrl);
|
|
122
99
|
await startMcpServer();
|
package/dist/index.d.ts
CHANGED
|
@@ -130,6 +130,7 @@ declare const PAYLOAD_FIELD: {
|
|
|
130
130
|
type WebhookKind = keyof typeof PAYLOAD_FIELD;
|
|
131
131
|
|
|
132
132
|
declare function postWebhook(url: string, text: string, kind?: WebhookKind, fetchImpl?: FetchLike): Promise<void>;
|
|
133
|
+
declare function inferWebhookKind(url: string): WebhookKind | null;
|
|
133
134
|
|
|
134
135
|
interface Provider {
|
|
135
136
|
readonly kind: ProviderKind;
|
|
@@ -279,6 +280,8 @@ declare function toMarkdown(report: StandupReport, lang?: string): string;
|
|
|
279
280
|
|
|
280
281
|
declare function buildReport(provider: Provider, today: Date, lang?: string, lookbackDays?: number): Promise<StandupReport>;
|
|
281
282
|
|
|
283
|
+
declare function readStandupSkillBody(): string;
|
|
284
|
+
|
|
282
285
|
declare function extractErrors(rawTrace: string, limit?: number): string[];
|
|
283
286
|
|
|
284
|
-
export { type ActiveDay, type ActivityEvent, ApiError, type Blocker, type Bucket, ConfigError, DIAGNOSIS_UNAVAILABLE, type FetchLike, GITHUB_LABELS, GITLAB_LABELS, GitHubProvider, GitLabProvider, type Identity, type MergeRequest, type Provider, type ProviderKind, type ProviderLabels, RETRY_ATTEMPTS, RETRY_BACKOFF_MS, type Review, STALE_DAYS, type SelectOptions, type StandupReport, approvedBy, assertUsable, buildReport, buildUrl, chooseKind, classify, connect, countChangesRequested, degradable, extractErrors, ghHosts, ghToken, glabHosts, glabToken, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, repoFromUrl, resolveHost, resolveToken, sendWithRetry, toMarkdown, undiagnosed, unreachable };
|
|
287
|
+
export { type ActiveDay, type ActivityEvent, ApiError, type Blocker, type Bucket, ConfigError, DIAGNOSIS_UNAVAILABLE, type FetchLike, GITHUB_LABELS, GITLAB_LABELS, GitHubProvider, GitLabProvider, type Identity, type MergeRequest, type Provider, type ProviderKind, type ProviderLabels, RETRY_ATTEMPTS, RETRY_BACKOFF_MS, type Review, STALE_DAYS, type SelectOptions, type StandupReport, approvedBy, assertUsable, buildReport, buildUrl, chooseKind, classify, connect, countChangesRequested, degradable, extractErrors, ghHosts, ghToken, glabHosts, glabToken, inferWebhookKind, isoDay, label, latestStateByReviewer, localAt, mapEvent, markMissingPipelines, normalizeChecks, parseGlabHosts, parseLoggedInHosts, postWebhook, previousActiveDays, readStandupSkillBody, repoFromUrl, resolveHost, resolveToken, sendWithRetry, toMarkdown, undiagnosed, unreachable };
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
ghToken,
|
|
25
25
|
glabHosts,
|
|
26
26
|
glabToken,
|
|
27
|
+
inferWebhookKind,
|
|
27
28
|
isoDay,
|
|
28
29
|
label,
|
|
29
30
|
latestStateByReviewer,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
parseLoggedInHosts,
|
|
36
37
|
postWebhook,
|
|
37
38
|
previousActiveDays,
|
|
39
|
+
readStandupSkillBody,
|
|
38
40
|
repoFromUrl,
|
|
39
41
|
resolveHost,
|
|
40
42
|
resolveToken,
|
|
@@ -42,7 +44,7 @@ import {
|
|
|
42
44
|
toMarkdown,
|
|
43
45
|
undiagnosed,
|
|
44
46
|
unreachable
|
|
45
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-S2PFIUNI.js";
|
|
46
48
|
export {
|
|
47
49
|
ApiError,
|
|
48
50
|
ConfigError,
|
|
@@ -68,6 +70,7 @@ export {
|
|
|
68
70
|
ghToken,
|
|
69
71
|
glabHosts,
|
|
70
72
|
glabToken,
|
|
73
|
+
inferWebhookKind,
|
|
71
74
|
isoDay,
|
|
72
75
|
label,
|
|
73
76
|
latestStateByReviewer,
|
|
@@ -79,6 +82,7 @@ export {
|
|
|
79
82
|
parseLoggedInHosts,
|
|
80
83
|
postWebhook,
|
|
81
84
|
previousActiveDays,
|
|
85
|
+
readStandupSkillBody,
|
|
82
86
|
repoFromUrl,
|
|
83
87
|
resolveHost,
|
|
84
88
|
resolveToken,
|
package/dist/mcp/server.js
CHANGED
|
@@ -31,6 +31,41 @@ function packageVersion(moduleUrl) {
|
|
|
31
31
|
return manifest.version;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// src/notify/notify.constants.ts
|
|
35
|
+
var PAYLOAD_FIELD = { slack: "text", discord: "content" };
|
|
36
|
+
|
|
37
|
+
// src/notify/notify.ts
|
|
38
|
+
async function postWebhook(url, text2, kind = "slack", fetchImpl = fetch) {
|
|
39
|
+
const field = PAYLOAD_FIELD[kind];
|
|
40
|
+
if (!field) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Unknown webhook kind "${kind}". Use one of: ${Object.keys(PAYLOAD_FIELD).join(", ")}.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const response = await fetchImpl(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ [field]: text2 })
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`Webhook rejected the message: HTTP ${response.status}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
var WEBHOOK_HOSTS = {
|
|
55
|
+
"hooks.slack.com": "slack",
|
|
56
|
+
"discord.com": "discord",
|
|
57
|
+
"discordapp.com": "discord",
|
|
58
|
+
"ptb.discord.com": "discord",
|
|
59
|
+
"canary.discord.com": "discord"
|
|
60
|
+
};
|
|
61
|
+
function inferWebhookKind(url) {
|
|
62
|
+
try {
|
|
63
|
+
return WEBHOOK_HOSTS[new URL(url).hostname] ?? null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
34
69
|
// src/config/config.ts
|
|
35
70
|
import { execFileSync } from "child_process";
|
|
36
71
|
|
|
@@ -1002,6 +1037,21 @@ function connect(options = {}) {
|
|
|
1002
1037
|
return new GitLabProvider(host, token);
|
|
1003
1038
|
}
|
|
1004
1039
|
|
|
1040
|
+
// src/skill/skill.ts
|
|
1041
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1042
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1043
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1044
|
+
function readStandupSkillBody() {
|
|
1045
|
+
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1046
|
+
const skillPath = join2(findPackageRoot(moduleDir), "skills", "standup", "SKILL.md");
|
|
1047
|
+
if (!existsSync2(skillPath)) {
|
|
1048
|
+
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1049
|
+
}
|
|
1050
|
+
const content = readFileSync2(skillPath, "utf8");
|
|
1051
|
+
const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
1052
|
+
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1005
1055
|
// src/report/report.constants.ts
|
|
1006
1056
|
var LOOKBACK_DAYS = 21;
|
|
1007
1057
|
|
|
@@ -1044,15 +1094,16 @@ async function collect(options = {}) {
|
|
|
1044
1094
|
const provider = options.providerImpl ?? connect({ provider: options.provider, host: options.host, token: options.token });
|
|
1045
1095
|
return buildReport(provider, /* @__PURE__ */ new Date(), options.lang ?? "en");
|
|
1046
1096
|
}
|
|
1097
|
+
var STANDUP_TOOL_DESCRIPTION = 'Collect merge-request-based standup data from GitLab or GitHub. Returns one JSON object: `today`, `previousDays[]` with the events of each, `todayEvents[]`, `myMrs[]` bucketed ready / blocked / draft / stale, `reviews[]` waiting on you, and `blockers[]` carrying the error lines read out of each failed pipeline job log.\n\nCall it once at the start of a working day, to write a standup note. It is a snapshot, not a search API: it cannot fetch one named merge request, reach further back than the previous working day, or filter by project.\n\nRead-only, and credentials never come from an argument \u2014 they come from the environment or a logged-in gh / glab session. A rejected token, a refused resource or a rate limit fails the call with the host\'s own message, after two retries on transient server errors. A blocker whose diagnosis could not be fetched is still returned, with `job: "unknown"`, so a red pipeline is never silently dropped.';
|
|
1047
1098
|
var STANDUP_TOOL_SCHEMA = {
|
|
1048
1099
|
provider: z.enum(["github", "gitlab"]).optional().describe(
|
|
1049
|
-
"Which provider to read. Omit to auto-detect, in this order: a recognisable host, STANDUP_PROVIDER, a GITHUB_*/GITLAB_* environment pair, then whichever of the gh / glab CLIs is logged in."
|
|
1100
|
+
"Which provider to read. Omit to auto-detect, in this order: a recognisable host, STANDUP_PROVIDER, a GITHUB_*/GITLAB_* environment pair, then whichever of the gh / glab CLIs is logged in. Pass it when both are configured \u2014 ambiguity fails the call rather than being guessed at."
|
|
1050
1101
|
),
|
|
1051
1102
|
host: z.string().optional().describe(
|
|
1052
|
-
"Self-hosted host, without a scheme, e.g. gitlab.example.com or github.example.com. GitHub defaults to github.com
|
|
1103
|
+
"Self-hosted host, without a scheme, e.g. gitlab.example.com or github.example.com. Required for GitLab, which has no default host; optional for GitHub, which defaults to github.com. A recognisable host also settles `provider` on its own, so the two are rarely both needed."
|
|
1053
1104
|
),
|
|
1054
1105
|
lang: z.enum(["en", "tr"]).optional().describe(
|
|
1055
|
-
"Language for the date labels inside the returned JSON
|
|
1106
|
+
"Language for the date labels inside the returned JSON: en (default) or tr. It relabels dates and nothing else \u2014 no field is translated, and the standup note itself is written by the caller, in whatever language they are speaking."
|
|
1056
1107
|
)
|
|
1057
1108
|
};
|
|
1058
1109
|
async function runStandupTool(args, collector = collect) {
|
|
@@ -1063,14 +1114,71 @@ async function runStandupTool(args, collector = collect) {
|
|
|
1063
1114
|
});
|
|
1064
1115
|
return { content: [{ type: "text", text: JSON.stringify(report) }] };
|
|
1065
1116
|
}
|
|
1117
|
+
var WEBHOOK_URL_ENV = "STANDUP_WEBHOOK_URL";
|
|
1118
|
+
var POST_TOOL_DESCRIPTION = `Posts a finished standup note to a chat webhook. This one has a side effect: it sends a message other people will see, so only call it on a note the user has agreed to send.
|
|
1119
|
+
|
|
1120
|
+
The webhook URL is read from ${WEBHOOK_URL_ENV}, never from an argument \u2014 a webhook URL is a credential, since anyone holding it can post to the channel. Slack and Discord payload shapes are supported; the shape is inferred from the URL host, and \`kind\` is only needed for a proxied or self-hosted endpoint whose host gives nothing away. A missing URL, an unrecognised host, or a webhook that rejects the message comes back as an error rather than a silent success.`;
|
|
1121
|
+
var POST_TOOL_SCHEMA = {
|
|
1122
|
+
text: z.string().min(1).describe(
|
|
1123
|
+
"The note to post, as the chat should render it. Slack and Discord both take Markdown, so send the written note rather than the raw JSON from get_standup_data."
|
|
1124
|
+
),
|
|
1125
|
+
kind: z.enum(["slack", "discord"]).optional().describe(
|
|
1126
|
+
'Which payload shape to send: slack posts {"text"}, discord posts {"content"}. Omit it \u2014 the shape is inferred from the webhook host. Pass it only when the host is not recognisable, e.g. a proxy in front of the real webhook.'
|
|
1127
|
+
)
|
|
1128
|
+
};
|
|
1129
|
+
var INSTRUCTIONS_TOOL_DESCRIPTION = "Returns the note-writing rules \u2014 the playbook for turning get_standup_data output into a standup note a person would actually say out loud: how to group the previous day by theme, how to derive today from open merge requests, and how to report a blocker from its job log. Read-only, no arguments, no network. Call it once before writing the first note; the rules do not change between calls.";
|
|
1130
|
+
function text(body) {
|
|
1131
|
+
return { content: [{ type: "text", text: body }] };
|
|
1132
|
+
}
|
|
1133
|
+
function failure(body) {
|
|
1134
|
+
return { content: [{ type: "text", text: body }], isError: true };
|
|
1135
|
+
}
|
|
1136
|
+
async function runPostTool(args, deps = {}) {
|
|
1137
|
+
const env = deps.env ?? process.env;
|
|
1138
|
+
const post = deps.post ?? postWebhook;
|
|
1139
|
+
const url = env[WEBHOOK_URL_ENV];
|
|
1140
|
+
if (!url) {
|
|
1141
|
+
return failure(
|
|
1142
|
+
`No webhook configured. Set ${WEBHOOK_URL_ENV} to the Slack or Discord webhook URL; it is deliberately not accepted as a tool argument.`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
const kind = args.kind ?? inferWebhookKind(url);
|
|
1146
|
+
if (!kind) {
|
|
1147
|
+
return failure(
|
|
1148
|
+
`Could not tell from the ${WEBHOOK_URL_ENV} host whether this is a Slack or a Discord webhook. Pass kind explicitly.`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
await post(url, args.text, kind);
|
|
1153
|
+
} catch (cause) {
|
|
1154
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
1155
|
+
return failure(`The ${kind} webhook did not accept the note: ${detail}`);
|
|
1156
|
+
}
|
|
1157
|
+
return text(`Posted the note to the configured ${kind} webhook.`);
|
|
1158
|
+
}
|
|
1159
|
+
async function runInstructionsTool() {
|
|
1160
|
+
return text(readStandupSkillBody());
|
|
1161
|
+
}
|
|
1066
1162
|
async function main() {
|
|
1067
1163
|
const server = new McpServer({ name: "standup-mr", version: packageVersion(import.meta.url) });
|
|
1068
1164
|
server.tool(
|
|
1069
1165
|
"get_standup_data",
|
|
1070
|
-
|
|
1166
|
+
STANDUP_TOOL_DESCRIPTION,
|
|
1071
1167
|
STANDUP_TOOL_SCHEMA,
|
|
1072
1168
|
async (args) => await runStandupTool(args)
|
|
1073
1169
|
);
|
|
1170
|
+
server.tool(
|
|
1171
|
+
"post_standup_note",
|
|
1172
|
+
POST_TOOL_DESCRIPTION,
|
|
1173
|
+
POST_TOOL_SCHEMA,
|
|
1174
|
+
async (args) => await runPostTool(args)
|
|
1175
|
+
);
|
|
1176
|
+
server.tool(
|
|
1177
|
+
"get_note_instructions",
|
|
1178
|
+
INSTRUCTIONS_TOOL_DESCRIPTION,
|
|
1179
|
+
{},
|
|
1180
|
+
async () => await runInstructionsTool()
|
|
1181
|
+
);
|
|
1074
1182
|
await server.connect(new StdioServerTransport());
|
|
1075
1183
|
}
|
|
1076
1184
|
var entry = process.argv[1];
|
|
@@ -1088,8 +1196,15 @@ if (entryUrl && import.meta.url === entryUrl) {
|
|
|
1088
1196
|
});
|
|
1089
1197
|
}
|
|
1090
1198
|
export {
|
|
1199
|
+
INSTRUCTIONS_TOOL_DESCRIPTION,
|
|
1200
|
+
POST_TOOL_DESCRIPTION,
|
|
1201
|
+
POST_TOOL_SCHEMA,
|
|
1202
|
+
STANDUP_TOOL_DESCRIPTION,
|
|
1091
1203
|
STANDUP_TOOL_SCHEMA,
|
|
1204
|
+
WEBHOOK_URL_ENV,
|
|
1092
1205
|
collect,
|
|
1093
1206
|
main,
|
|
1207
|
+
runInstructionsTool,
|
|
1208
|
+
runPostTool,
|
|
1094
1209
|
runStandupTool
|
|
1095
1210
|
};
|
package/llms-install.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Installing standup-mr
|
|
2
|
+
|
|
3
|
+
Instructions for an AI assistant setting this MCP server up on a user's
|
|
4
|
+
machine. Everything here is verified against the published package.
|
|
5
|
+
|
|
6
|
+
## What it needs
|
|
7
|
+
|
|
8
|
+
- Node.js 20 or newer. Nothing to clone, nothing to build — the server runs
|
|
9
|
+
straight from npm.
|
|
10
|
+
- Read access to the user's GitLab or GitHub. It never takes a credential as
|
|
11
|
+
a tool argument; credentials come from the environment or from a logged-in
|
|
12
|
+
`gh` / `glab` session.
|
|
13
|
+
|
|
14
|
+
## Config
|
|
15
|
+
|
|
16
|
+
Add this to the MCP settings file (for Cline,
|
|
17
|
+
`cline_mcp_settings.json`):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"standup": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
25
|
+
"env": {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That is the whole install when the user already has `gh` or `glab` logged in
|
|
32
|
+
— leave `env` empty and the server picks the session up.
|
|
33
|
+
|
|
34
|
+
## Credentials, when there is no CLI session
|
|
35
|
+
|
|
36
|
+
Add only the pair the user actually needs.
|
|
37
|
+
|
|
38
|
+
| Variable | When |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `GITHUB_TOKEN` | GitHub, no `gh` session |
|
|
41
|
+
| `GITHUB_HOST` | GitHub Enterprise only; defaults to `github.com` |
|
|
42
|
+
| `GITLAB_TOKEN` | GitLab, no `glab` session |
|
|
43
|
+
| `GITLAB_HOST` | **Required for GitLab** — there is no default host |
|
|
44
|
+
| `STANDUP_PROVIDER` | `github` or `gitlab`, when both are configured and the choice is ambiguous |
|
|
45
|
+
|
|
46
|
+
Ask the user for a token; do not invent one, and do not put a token anywhere
|
|
47
|
+
but `env`.
|
|
48
|
+
|
|
49
|
+
## The one tool
|
|
50
|
+
|
|
51
|
+
`get_standup_data` returns the previous working day's activity, open merge
|
|
52
|
+
requests bucketed by state (ready / blocked / draft / stale), pending
|
|
53
|
+
reviews, and the error lines from any failed pipeline job. All three
|
|
54
|
+
arguments are optional:
|
|
55
|
+
|
|
56
|
+
- `provider` — `github` or `gitlab`; omit to auto-detect
|
|
57
|
+
- `host` — self-hosted host, no scheme
|
|
58
|
+
- `lang` — `en` or `tr`; changes date labels in the JSON only
|
|
59
|
+
|
|
60
|
+
## Verifying the install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx -y standup-mr mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It speaks MCP over stdio and writes nothing else to stdout. A successful
|
|
67
|
+
`initialize` reports `{"name":"standup-mr","version":"<current>"}`.
|
|
68
|
+
|
|
69
|
+
If the user is not on an MCP client, `npx standup-mr fetch --markdown`
|
|
70
|
+
prints the same data as a digest.
|
|
71
|
+
|
|
72
|
+
## Writing the note
|
|
73
|
+
|
|
74
|
+
The tool returns data, not prose. The note-writing rules live in the bundled
|
|
75
|
+
skill; pour them into whatever instructions file the assistant reads:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx standup-mr instructions >> AGENTS.md
|
|
79
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "standup-mr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.Jubstaaa/standup-mr",
|
|
5
5
|
"description": "Standup notes from merge request state, not commit logs.",
|
|
6
6
|
"keywords": [
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"dist",
|
|
49
49
|
"skills",
|
|
50
50
|
"mcp/README.md",
|
|
51
|
+
"llms-install.md",
|
|
51
52
|
"README.md",
|
|
52
53
|
"CHANGELOG.md",
|
|
53
54
|
"LICENSE"
|