standup-mr 0.6.1 → 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 +46 -0
- package/README.md +82 -29
- package/dist/{chunk-NJO6LFYN.js → chunk-S7DAKN36.js} +147 -47
- package/dist/cli.js +19 -5
- package/dist/index.d.ts +60 -60
- package/dist/index.js +1 -1
- package/dist/mcp/server.js +221 -72
- package/llms-install.md +17 -12
- package/mcp/README.md +33 -28
- package/package.json +19 -4
package/dist/mcp/server.js
CHANGED
|
@@ -24,21 +24,15 @@ function findPackageRoot(startDir) {
|
|
|
24
24
|
}
|
|
25
25
|
function packageVersion(moduleUrl) {
|
|
26
26
|
const root = findPackageRoot(dirname(fileURLToPath(moduleUrl)));
|
|
27
|
-
const manifest = JSON.parse(
|
|
27
|
+
const manifest = JSON.parse(
|
|
28
|
+
readFileSync(join(root, "package.json"), "utf8")
|
|
29
|
+
);
|
|
28
30
|
if (!manifest.version) {
|
|
29
31
|
throw new Error(`package.json at ${root} has no version`);
|
|
30
32
|
}
|
|
31
33
|
return manifest.version;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
// src/notify/notify.constants.ts
|
|
35
|
-
var PAYLOAD_FIELD = {
|
|
36
|
-
slack: "text",
|
|
37
|
-
discord: "content",
|
|
38
|
-
"google-chat": "text"
|
|
39
|
-
};
|
|
40
|
-
var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
|
|
41
|
-
|
|
42
36
|
// src/render/chat.ts
|
|
43
37
|
var MARKDOWN_NATIVE = ["discord"];
|
|
44
38
|
var FENCE = /^```/;
|
|
@@ -59,6 +53,14 @@ function toChatText(markdown, kind) {
|
|
|
59
53
|
}).join("\n");
|
|
60
54
|
}
|
|
61
55
|
|
|
56
|
+
// src/notify/notify.constants.ts
|
|
57
|
+
var PAYLOAD_FIELD = {
|
|
58
|
+
"slack": "text",
|
|
59
|
+
"discord": "content",
|
|
60
|
+
"google-chat": "text"
|
|
61
|
+
};
|
|
62
|
+
var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
|
|
63
|
+
|
|
62
64
|
// src/notify/notify.ts
|
|
63
65
|
async function postWebhook(url, text2, kind = "slack", fetchImpl = fetch) {
|
|
64
66
|
const field = PAYLOAD_FIELD[kind];
|
|
@@ -73,7 +75,9 @@ async function postWebhook(url, text2, kind = "slack", fetchImpl = fetch) {
|
|
|
73
75
|
body: JSON.stringify({ [field]: toChatText(text2, kind) })
|
|
74
76
|
});
|
|
75
77
|
if (!response.ok) {
|
|
76
|
-
throw new Error(
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Webhook rejected the message: HTTP ${response.status}.`
|
|
80
|
+
);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
var WEBHOOK_HOSTS = {
|
|
@@ -168,7 +172,9 @@ function cliStatus(cli) {
|
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
function parseLoggedInHosts(statusOutput) {
|
|
171
|
-
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
175
|
+
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
176
|
+
(match) => match[1]
|
|
177
|
+
);
|
|
172
178
|
return [...new Set(hosts)].sort();
|
|
173
179
|
}
|
|
174
180
|
function glabHosts() {
|
|
@@ -184,13 +190,26 @@ function ghToken(host) {
|
|
|
184
190
|
return cliCapture("gh", ["auth", "token", "--hostname", host]);
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
// src/buckets/buckets.constants.ts
|
|
188
|
-
var STALE_DAYS = 14;
|
|
189
|
-
|
|
190
193
|
// src/dates/dates.constants.ts
|
|
191
194
|
var DAYS = {
|
|
192
|
-
en: [
|
|
193
|
-
|
|
195
|
+
en: [
|
|
196
|
+
"Sunday",
|
|
197
|
+
"Monday",
|
|
198
|
+
"Tuesday",
|
|
199
|
+
"Wednesday",
|
|
200
|
+
"Thursday",
|
|
201
|
+
"Friday",
|
|
202
|
+
"Saturday"
|
|
203
|
+
],
|
|
204
|
+
tr: [
|
|
205
|
+
"Pazar",
|
|
206
|
+
"Pazartesi",
|
|
207
|
+
"Sal\u0131",
|
|
208
|
+
"\xC7ar\u015Famba",
|
|
209
|
+
"Per\u015Fembe",
|
|
210
|
+
"Cuma",
|
|
211
|
+
"Cumartesi"
|
|
212
|
+
]
|
|
194
213
|
};
|
|
195
214
|
var MONTHS = {
|
|
196
215
|
en: [
|
|
@@ -261,10 +280,14 @@ function previousActiveDays(eventDates, today) {
|
|
|
261
280
|
}));
|
|
262
281
|
}
|
|
263
282
|
|
|
283
|
+
// src/buckets/buckets.constants.ts
|
|
284
|
+
var STALE_DAYS = 14;
|
|
285
|
+
|
|
264
286
|
// src/buckets/buckets.ts
|
|
265
287
|
function classify(mr, today, staleDays = STALE_DAYS) {
|
|
266
288
|
if (mr.draft) return "draft";
|
|
267
|
-
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
289
|
+
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
290
|
+
return "blocked";
|
|
268
291
|
const age = Math.round(
|
|
269
292
|
(Date.parse(`${isoDay(today)}T00:00:00Z`) - Date.parse(`${mr.updated}T00:00:00Z`)) / MS_PER_DAY
|
|
270
293
|
);
|
|
@@ -350,17 +373,25 @@ function retryAfter(response) {
|
|
|
350
373
|
const value = response.headers.get("retry-after");
|
|
351
374
|
if (!value) return "";
|
|
352
375
|
const seconds = Number(value);
|
|
353
|
-
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
376
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
377
|
+
return `; retry after ${value}`;
|
|
354
378
|
return `; retry after ${seconds}s`;
|
|
355
379
|
}
|
|
356
380
|
function assertUsable(response, host) {
|
|
357
381
|
if (response.ok || response.status === 404) return;
|
|
358
382
|
if (response.status === 403 || response.status === 429) {
|
|
359
383
|
if (remaining(response) === "0") {
|
|
360
|
-
throw new ApiError(
|
|
384
|
+
throw new ApiError(
|
|
385
|
+
`${host} rate limit reached${resetAt(response)}.`,
|
|
386
|
+
response.status
|
|
387
|
+
);
|
|
361
388
|
}
|
|
362
389
|
const retry = retryAfter(response);
|
|
363
|
-
if (retry)
|
|
390
|
+
if (retry)
|
|
391
|
+
throw new ApiError(
|
|
392
|
+
`${host} rate limit reached${retry}.`,
|
|
393
|
+
response.status
|
|
394
|
+
);
|
|
364
395
|
}
|
|
365
396
|
if (response.status === 401) {
|
|
366
397
|
throw new ApiError(
|
|
@@ -520,8 +551,8 @@ var GitHubProvider = class {
|
|
|
520
551
|
}
|
|
521
552
|
headers() {
|
|
522
553
|
return {
|
|
523
|
-
authorization: `Bearer ${this.token}`,
|
|
524
|
-
accept: "application/vnd.github+json",
|
|
554
|
+
"authorization": `Bearer ${this.token}`,
|
|
555
|
+
"accept": "application/vnd.github+json",
|
|
525
556
|
"x-github-api-version": API_VERSION
|
|
526
557
|
};
|
|
527
558
|
}
|
|
@@ -573,11 +604,14 @@ var GitHubProvider = class {
|
|
|
573
604
|
return rows;
|
|
574
605
|
}
|
|
575
606
|
async getLogText(path) {
|
|
576
|
-
const first = await this.send(buildUrl(this.api, path), {
|
|
607
|
+
const first = await this.send(buildUrl(this.api, path), {
|
|
608
|
+
redirect: "manual"
|
|
609
|
+
});
|
|
577
610
|
if (first.status === 404) return "";
|
|
578
611
|
const location = first.headers.get("location");
|
|
579
612
|
if (!location) {
|
|
580
|
-
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
613
|
+
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
614
|
+
return "";
|
|
581
615
|
assertUsable(first, this.host);
|
|
582
616
|
return first.ok ? await first.text() : "";
|
|
583
617
|
}
|
|
@@ -620,7 +654,9 @@ var GitHubProvider = class {
|
|
|
620
654
|
async shapePr(item) {
|
|
621
655
|
const project = repoFromUrl(item.repository_url);
|
|
622
656
|
const iid = item.number;
|
|
623
|
-
const pull = await this.getJson(
|
|
657
|
+
const pull = await this.getJson(
|
|
658
|
+
`repos/${project}/pulls/${iid}`
|
|
659
|
+
);
|
|
624
660
|
if (!pull) return null;
|
|
625
661
|
const sha = pull.head?.sha ?? "";
|
|
626
662
|
const [checks, reviews] = await Promise.all([
|
|
@@ -628,9 +664,15 @@ var GitHubProvider = class {
|
|
|
628
664
|
`repos/${project}/commits/${sha}/check-runs`,
|
|
629
665
|
{ per_page: PAGE_SIZE }
|
|
630
666
|
) : Promise.resolve(null),
|
|
631
|
-
this.getPaged(
|
|
667
|
+
this.getPaged(
|
|
668
|
+
`repos/${project}/pulls/${iid}/reviews`,
|
|
669
|
+
{},
|
|
670
|
+
2
|
|
671
|
+
)
|
|
632
672
|
]);
|
|
633
|
-
const { pipeline, pipelineId } = normalizeChecks(
|
|
673
|
+
const { pipeline, pipelineId } = normalizeChecks(
|
|
674
|
+
checks?.check_runs ?? []
|
|
675
|
+
);
|
|
634
676
|
return {
|
|
635
677
|
provider: "github",
|
|
636
678
|
project,
|
|
@@ -669,7 +711,9 @@ var GitHubProvider = class {
|
|
|
669
711
|
`is:pr is:open review-requested:${identity.username}`,
|
|
670
712
|
SEARCH_CAP
|
|
671
713
|
);
|
|
672
|
-
const cutoff = isoDay(
|
|
714
|
+
const cutoff = isoDay(
|
|
715
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY)
|
|
716
|
+
);
|
|
673
717
|
const rows = await Promise.all(
|
|
674
718
|
items.map(async (item) => {
|
|
675
719
|
const project = repoFromUrl(item.repository_url);
|
|
@@ -735,7 +779,10 @@ var GitHubProvider = class {
|
|
|
735
779
|
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
736
780
|
);
|
|
737
781
|
if (!failed) return null;
|
|
738
|
-
const summary = [
|
|
782
|
+
const summary = [
|
|
783
|
+
failed.output?.summary ?? "",
|
|
784
|
+
failed.output?.text ?? ""
|
|
785
|
+
].filter(Boolean).join("\n");
|
|
739
786
|
const errors = extractErrors(summary);
|
|
740
787
|
if (errors.length === 0) return null;
|
|
741
788
|
return {
|
|
@@ -795,7 +842,10 @@ var GitLabProvider = class {
|
|
|
795
842
|
const response = await sendWithRetry(
|
|
796
843
|
this.fetchImpl,
|
|
797
844
|
url,
|
|
798
|
-
|
|
845
|
+
// Bearer, not PRIVATE-TOKEN: GitLab accepts personal access tokens
|
|
846
|
+
// either way, but OAuth tokens — glab's browser login, and the
|
|
847
|
+
// remote server's sign-in — only as a bearer.
|
|
848
|
+
{ headers: { Authorization: `Bearer ${this.token}` } },
|
|
799
849
|
this.host
|
|
800
850
|
);
|
|
801
851
|
assertUsable(response, this.host);
|
|
@@ -848,7 +898,9 @@ var GitLabProvider = class {
|
|
|
848
898
|
const raw = await this.getPaged("events", {
|
|
849
899
|
after: isoDay(since)
|
|
850
900
|
});
|
|
851
|
-
const ids = [
|
|
901
|
+
const ids = [
|
|
902
|
+
...new Set(raw.map((e) => e.project_id).filter(Boolean))
|
|
903
|
+
];
|
|
852
904
|
const paths = new Map(
|
|
853
905
|
await Promise.all(
|
|
854
906
|
ids.map(async (id) => [id, await this.projectPath(id)])
|
|
@@ -859,7 +911,7 @@ var GitLabProvider = class {
|
|
|
859
911
|
return {
|
|
860
912
|
at: String(event.created_at).slice(0, 16),
|
|
861
913
|
action: event.action_name,
|
|
862
|
-
project: paths.get(event.project_id) ?? "",
|
|
914
|
+
project: paths.get(event.project_id ?? -1) ?? "",
|
|
863
915
|
targetType: event.target_type ?? "",
|
|
864
916
|
title: event.target_title ?? "",
|
|
865
917
|
branch: push.ref ?? "",
|
|
@@ -871,7 +923,9 @@ var GitLabProvider = class {
|
|
|
871
923
|
async countUnresolved(projectId, iid) {
|
|
872
924
|
const discussions = await this.getJson(
|
|
873
925
|
`projects/${projectId}/merge_requests/${iid}/discussions`,
|
|
874
|
-
{
|
|
926
|
+
{
|
|
927
|
+
per_page: PAGE_SIZE2
|
|
928
|
+
}
|
|
875
929
|
);
|
|
876
930
|
if (!discussions) return 0;
|
|
877
931
|
return discussions.filter((discussion) => {
|
|
@@ -924,7 +978,9 @@ var GitLabProvider = class {
|
|
|
924
978
|
async approvedByMe(projectId, iid, uid) {
|
|
925
979
|
const approvals = await this.getJson(`projects/${projectId}/merge_requests/${iid}/approvals`);
|
|
926
980
|
if (!approvals) return false;
|
|
927
|
-
return (approvals.approved_by ?? []).some(
|
|
981
|
+
return (approvals.approved_by ?? []).some(
|
|
982
|
+
(entry2) => entry2.user?.id === uid
|
|
983
|
+
);
|
|
928
984
|
}
|
|
929
985
|
async getReviews(identity, today) {
|
|
930
986
|
const raw = await this.getPaged("merge_requests", {
|
|
@@ -932,7 +988,9 @@ var GitLabProvider = class {
|
|
|
932
988
|
state: "opened",
|
|
933
989
|
reviewer_id: identity.id
|
|
934
990
|
});
|
|
935
|
-
const cutoff = isoDay(
|
|
991
|
+
const cutoff = isoDay(
|
|
992
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY)
|
|
993
|
+
);
|
|
936
994
|
const rows = await Promise.all(
|
|
937
995
|
raw.map(async (mr) => ({
|
|
938
996
|
provider: "gitlab",
|
|
@@ -944,7 +1002,11 @@ var GitLabProvider = class {
|
|
|
944
1002
|
draft: mr.draft,
|
|
945
1003
|
url: mr.web_url,
|
|
946
1004
|
fresh: String(mr.updated_at).slice(0, 10) >= cutoff,
|
|
947
|
-
approvedByMe: await this.approvedByMe(
|
|
1005
|
+
approvedByMe: await this.approvedByMe(
|
|
1006
|
+
mr.project_id,
|
|
1007
|
+
mr.iid,
|
|
1008
|
+
identity.id
|
|
1009
|
+
)
|
|
948
1010
|
}))
|
|
949
1011
|
);
|
|
950
1012
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
@@ -956,7 +1018,9 @@ var GitLabProvider = class {
|
|
|
956
1018
|
) ?? [];
|
|
957
1019
|
const job = jobs.find((j) => j.status === "failed");
|
|
958
1020
|
if (!job) return null;
|
|
959
|
-
const trace = await this.getText(
|
|
1021
|
+
const trace = await this.getText(
|
|
1022
|
+
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
1023
|
+
);
|
|
960
1024
|
return {
|
|
961
1025
|
provider: "gitlab",
|
|
962
1026
|
project: mr.project,
|
|
@@ -989,7 +1053,12 @@ function hostFrom(cliHost, envHost, probe, fallback, labels) {
|
|
|
989
1053
|
if (cliHost) return cliHost;
|
|
990
1054
|
if (envHost) return envHost;
|
|
991
1055
|
const found = probe();
|
|
992
|
-
return resolveHost(
|
|
1056
|
+
return resolveHost(
|
|
1057
|
+
void 0,
|
|
1058
|
+
void 0,
|
|
1059
|
+
found.length > 0 ? found : fallback,
|
|
1060
|
+
labels
|
|
1061
|
+
);
|
|
993
1062
|
}
|
|
994
1063
|
function chooseKind(options = {}) {
|
|
995
1064
|
const env = options.env ?? process.env;
|
|
@@ -1042,7 +1111,13 @@ function connect(options = {}) {
|
|
|
1042
1111
|
const env = options.env ?? process.env;
|
|
1043
1112
|
const probe = options.probe ?? DEFAULT_PROBE;
|
|
1044
1113
|
if (chooseKind(options) === "github") {
|
|
1045
|
-
const host2 = hostFrom(
|
|
1114
|
+
const host2 = hostFrom(
|
|
1115
|
+
options.host,
|
|
1116
|
+
env.GITHUB_HOST,
|
|
1117
|
+
probe.github,
|
|
1118
|
+
[DOT_COM],
|
|
1119
|
+
GITHUB_LABELS
|
|
1120
|
+
);
|
|
1046
1121
|
const token2 = resolveToken(
|
|
1047
1122
|
host2,
|
|
1048
1123
|
options.token,
|
|
@@ -1052,7 +1127,13 @@ function connect(options = {}) {
|
|
|
1052
1127
|
);
|
|
1053
1128
|
return new GitHubProvider(host2, token2);
|
|
1054
1129
|
}
|
|
1055
|
-
const host = hostFrom(
|
|
1130
|
+
const host = hostFrom(
|
|
1131
|
+
options.host,
|
|
1132
|
+
env.GITLAB_HOST,
|
|
1133
|
+
probe.gitlab,
|
|
1134
|
+
[],
|
|
1135
|
+
GITLAB_LABELS
|
|
1136
|
+
);
|
|
1056
1137
|
const token = resolveToken(
|
|
1057
1138
|
host,
|
|
1058
1139
|
options.token,
|
|
@@ -1063,21 +1144,6 @@ function connect(options = {}) {
|
|
|
1063
1144
|
return new GitLabProvider(host, token);
|
|
1064
1145
|
}
|
|
1065
1146
|
|
|
1066
|
-
// src/skill/skill.ts
|
|
1067
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1068
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
1069
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1070
|
-
function readStandupSkillBody() {
|
|
1071
|
-
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1072
|
-
const skillPath = join2(findPackageRoot(moduleDir), "skills", "standup", "SKILL.md");
|
|
1073
|
-
if (!existsSync2(skillPath)) {
|
|
1074
|
-
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1075
|
-
}
|
|
1076
|
-
const content = readFileSync2(skillPath, "utf8");
|
|
1077
|
-
const withoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
|
|
1078
|
-
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
1147
|
// src/report/report.constants.ts
|
|
1082
1148
|
var LOOKBACK_DAYS = 21;
|
|
1083
1149
|
|
|
@@ -1115,9 +1181,36 @@ async function buildReport(provider, today, lang = "en", lookbackDays = LOOKBACK
|
|
|
1115
1181
|
};
|
|
1116
1182
|
}
|
|
1117
1183
|
|
|
1184
|
+
// src/skill/skill.ts
|
|
1185
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1186
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1187
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1188
|
+
function readStandupSkillBody() {
|
|
1189
|
+
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1190
|
+
const skillPath = join2(
|
|
1191
|
+
findPackageRoot(moduleDir),
|
|
1192
|
+
"skills",
|
|
1193
|
+
"standup",
|
|
1194
|
+
"SKILL.md"
|
|
1195
|
+
);
|
|
1196
|
+
if (!existsSync2(skillPath)) {
|
|
1197
|
+
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1198
|
+
}
|
|
1199
|
+
const content = readFileSync2(skillPath, "utf8");
|
|
1200
|
+
const withoutFrontmatter = content.replace(
|
|
1201
|
+
/^---\r?\n[\s\S]*?\r?\n---\r?\n/,
|
|
1202
|
+
""
|
|
1203
|
+
);
|
|
1204
|
+
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1118
1207
|
// mcp/server.ts
|
|
1119
1208
|
async function collect(options = {}) {
|
|
1120
|
-
const provider = options.providerImpl ?? connect({
|
|
1209
|
+
const provider = options.providerImpl ?? connect({
|
|
1210
|
+
provider: options.provider,
|
|
1211
|
+
host: options.host,
|
|
1212
|
+
token: options.token
|
|
1213
|
+
});
|
|
1121
1214
|
return buildReport(provider, /* @__PURE__ */ new Date(), options.lang ?? "en");
|
|
1122
1215
|
}
|
|
1123
1216
|
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.';
|
|
@@ -1138,7 +1231,9 @@ async function runStandupTool(args, collector = collect) {
|
|
|
1138
1231
|
host: args.host,
|
|
1139
1232
|
lang: args.lang
|
|
1140
1233
|
});
|
|
1141
|
-
return {
|
|
1234
|
+
return {
|
|
1235
|
+
content: [{ type: "text", text: JSON.stringify(report) }]
|
|
1236
|
+
};
|
|
1142
1237
|
}
|
|
1143
1238
|
var WEBHOOK_URL_ENV = "STANDUP_WEBHOOK_URL";
|
|
1144
1239
|
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.
|
|
@@ -1185,27 +1280,72 @@ async function runPostTool(args, deps = {}) {
|
|
|
1185
1280
|
async function runInstructionsTool() {
|
|
1186
1281
|
return text(readStandupSkillBody());
|
|
1187
1282
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1283
|
+
var STANDUP_TOOL_ANNOTATIONS = {
|
|
1284
|
+
readOnlyHint: true,
|
|
1285
|
+
destructiveHint: false,
|
|
1286
|
+
idempotentHint: true,
|
|
1287
|
+
openWorldHint: true
|
|
1288
|
+
};
|
|
1289
|
+
var POST_TOOL_ANNOTATIONS = {
|
|
1290
|
+
readOnlyHint: false,
|
|
1291
|
+
destructiveHint: false,
|
|
1292
|
+
idempotentHint: false,
|
|
1293
|
+
openWorldHint: true
|
|
1294
|
+
};
|
|
1295
|
+
var INSTRUCTIONS_TOOL_ANNOTATIONS = {
|
|
1296
|
+
readOnlyHint: true,
|
|
1297
|
+
destructiveHint: false,
|
|
1298
|
+
idempotentHint: true,
|
|
1299
|
+
openWorldHint: false
|
|
1300
|
+
};
|
|
1301
|
+
var SERVER_ICONS = [
|
|
1302
|
+
{
|
|
1303
|
+
src: "https://raw.githubusercontent.com/Jubstaaa/standup-mr/main/assets/icon-512.png",
|
|
1304
|
+
mimeType: "image/png",
|
|
1305
|
+
sizes: ["512x512"]
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
src: "https://raw.githubusercontent.com/Jubstaaa/standup-mr/main/assets/icon.svg",
|
|
1309
|
+
mimeType: "image/svg+xml",
|
|
1310
|
+
sizes: ["any"]
|
|
1311
|
+
}
|
|
1312
|
+
];
|
|
1313
|
+
function createServer() {
|
|
1314
|
+
const server = new McpServer({
|
|
1315
|
+
name: "standup-mr",
|
|
1316
|
+
version: packageVersion(import.meta.url),
|
|
1317
|
+
icons: SERVER_ICONS
|
|
1318
|
+
});
|
|
1319
|
+
server.registerTool(
|
|
1191
1320
|
"get_standup_data",
|
|
1192
|
-
|
|
1193
|
-
|
|
1321
|
+
{
|
|
1322
|
+
description: STANDUP_TOOL_DESCRIPTION,
|
|
1323
|
+
inputSchema: STANDUP_TOOL_SCHEMA,
|
|
1324
|
+
annotations: STANDUP_TOOL_ANNOTATIONS
|
|
1325
|
+
},
|
|
1194
1326
|
async (args) => await runStandupTool(args)
|
|
1195
1327
|
);
|
|
1196
|
-
server.
|
|
1328
|
+
server.registerTool(
|
|
1197
1329
|
"post_standup_note",
|
|
1198
|
-
|
|
1199
|
-
|
|
1330
|
+
{
|
|
1331
|
+
description: POST_TOOL_DESCRIPTION,
|
|
1332
|
+
inputSchema: POST_TOOL_SCHEMA,
|
|
1333
|
+
annotations: POST_TOOL_ANNOTATIONS
|
|
1334
|
+
},
|
|
1200
1335
|
async (args) => await runPostTool(args)
|
|
1201
1336
|
);
|
|
1202
|
-
server.
|
|
1337
|
+
server.registerTool(
|
|
1203
1338
|
"get_note_instructions",
|
|
1204
|
-
|
|
1205
|
-
|
|
1339
|
+
{
|
|
1340
|
+
description: INSTRUCTIONS_TOOL_DESCRIPTION,
|
|
1341
|
+
annotations: INSTRUCTIONS_TOOL_ANNOTATIONS
|
|
1342
|
+
},
|
|
1206
1343
|
async () => await runInstructionsTool()
|
|
1207
1344
|
);
|
|
1208
|
-
|
|
1345
|
+
return server;
|
|
1346
|
+
}
|
|
1347
|
+
async function main() {
|
|
1348
|
+
await createServer().connect(new StdioServerTransport());
|
|
1209
1349
|
}
|
|
1210
1350
|
var entry = process.argv[1];
|
|
1211
1351
|
var entryUrl;
|
|
@@ -1216,21 +1356,30 @@ try {
|
|
|
1216
1356
|
}
|
|
1217
1357
|
if (entryUrl && import.meta.url === entryUrl) {
|
|
1218
1358
|
main().catch((error) => {
|
|
1219
|
-
process.stderr.write(
|
|
1220
|
-
|
|
1359
|
+
process.stderr.write(
|
|
1360
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
1361
|
+
`
|
|
1362
|
+
);
|
|
1221
1363
|
process.exitCode = 1;
|
|
1222
1364
|
});
|
|
1223
1365
|
}
|
|
1224
1366
|
export {
|
|
1367
|
+
INSTRUCTIONS_TOOL_ANNOTATIONS,
|
|
1225
1368
|
INSTRUCTIONS_TOOL_DESCRIPTION,
|
|
1369
|
+
POST_TOOL_ANNOTATIONS,
|
|
1226
1370
|
POST_TOOL_DESCRIPTION,
|
|
1227
1371
|
POST_TOOL_SCHEMA,
|
|
1372
|
+
SERVER_ICONS,
|
|
1373
|
+
STANDUP_TOOL_ANNOTATIONS,
|
|
1228
1374
|
STANDUP_TOOL_DESCRIPTION,
|
|
1229
1375
|
STANDUP_TOOL_SCHEMA,
|
|
1230
1376
|
WEBHOOK_URL_ENV,
|
|
1231
1377
|
collect,
|
|
1378
|
+
createServer,
|
|
1379
|
+
failure,
|
|
1232
1380
|
main,
|
|
1233
1381
|
runInstructionsTool,
|
|
1234
1382
|
runPostTool,
|
|
1235
|
-
runStandupTool
|
|
1383
|
+
runStandupTool,
|
|
1384
|
+
text
|
|
1236
1385
|
};
|
package/llms-install.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
Instructions for an AI assistant setting this MCP server up on a user's
|
|
4
4
|
machine. Everything here is verified against the published package.
|
|
5
5
|
|
|
6
|
+
If the user is on claude.ai (web or mobile) rather than a client that runs
|
|
7
|
+
local processes, none of this applies: tell them to add
|
|
8
|
+
`https://standup.ilkerbalcilar.com/mcp` under Settings → Connectors → Add custom
|
|
9
|
+
connector and sign in on the page that opens.
|
|
10
|
+
|
|
6
11
|
## What it needs
|
|
7
12
|
|
|
8
13
|
- Node.js 20 or newer. Nothing to clone, nothing to build — the server runs
|
|
@@ -18,13 +23,13 @@ Add this to the MCP settings file (for Cline,
|
|
|
18
23
|
|
|
19
24
|
```json
|
|
20
25
|
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"standup": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
30
|
+
"env": {}
|
|
31
|
+
}
|
|
26
32
|
}
|
|
27
|
-
}
|
|
28
33
|
}
|
|
29
34
|
```
|
|
30
35
|
|
|
@@ -35,12 +40,12 @@ That is the whole install when the user already has `gh` or `glab` logged in
|
|
|
35
40
|
|
|
36
41
|
Add only the pair the user actually needs.
|
|
37
42
|
|
|
38
|
-
| Variable
|
|
39
|
-
|
|
40
|
-
| `GITHUB_TOKEN`
|
|
41
|
-
| `GITHUB_HOST`
|
|
42
|
-
| `GITLAB_TOKEN`
|
|
43
|
-
| `GITLAB_HOST`
|
|
43
|
+
| Variable | When |
|
|
44
|
+
| ------------------ | -------------------------------------------------------------------------- |
|
|
45
|
+
| `GITHUB_TOKEN` | GitHub, no `gh` session |
|
|
46
|
+
| `GITHUB_HOST` | GitHub Enterprise only; defaults to `github.com` |
|
|
47
|
+
| `GITLAB_TOKEN` | GitLab, no `glab` session |
|
|
48
|
+
| `GITLAB_HOST` | **Required for GitLab** — there is no default host |
|
|
44
49
|
| `STANDUP_PROVIDER` | `github` or `gitlab`, when both are configured and the choice is ambiguous |
|
|
45
50
|
|
|
46
51
|
Ask the user for a token; do not invent one, and do not put a token anywhere
|
package/mcp/README.md
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
# MCP server
|
|
2
2
|
|
|
3
|
-
Exposes
|
|
3
|
+
Exposes three tools — `get_standup_data`, `get_note_instructions` and
|
|
4
|
+
`post_standup_note` — for either GitLab or GitHub. `get_standup_data` returns
|
|
4
5
|
the same JSON as `standup fetch`.
|
|
5
6
|
|
|
7
|
+
This is the local, stdio server: it runs on your machine and reads tokens from
|
|
8
|
+
its environment. On claude.ai (web or mobile), use the hosted connector instead
|
|
9
|
+
— see [claude.ai (web and mobile)](../README.md#claudeai-web-and-mobile).
|
|
10
|
+
|
|
6
11
|
## Configure
|
|
7
12
|
|
|
8
13
|
GitLab:
|
|
9
14
|
|
|
10
15
|
```json
|
|
11
16
|
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"standup": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
21
|
+
"env": {
|
|
22
|
+
"GITLAB_HOST": "gitlab.example.com",
|
|
23
|
+
"GITLAB_TOKEN": "glpat-..."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
|
-
}
|
|
22
27
|
}
|
|
23
28
|
```
|
|
24
29
|
|
|
@@ -26,16 +31,16 @@ GitHub:
|
|
|
26
31
|
|
|
27
32
|
```json
|
|
28
33
|
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"standup": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
38
|
+
"env": {
|
|
39
|
+
"GITHUB_HOST": "github.com",
|
|
40
|
+
"GITHUB_TOKEN": "ghp_..."
|
|
41
|
+
}
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
|
-
}
|
|
39
44
|
}
|
|
40
45
|
```
|
|
41
46
|
|
|
@@ -46,16 +51,16 @@ If you're working from a local clone instead of the published package, point
|
|
|
46
51
|
|
|
47
52
|
```json
|
|
48
53
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"standup": {
|
|
56
|
+
"command": "node",
|
|
57
|
+
"args": ["/absolute/path/to/standup-mr/dist/mcp/server.js"],
|
|
58
|
+
"env": {
|
|
59
|
+
"GITHUB_HOST": "github.com",
|
|
60
|
+
"GITHUB_TOKEN": "ghp_..."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
|
-
}
|
|
59
64
|
}
|
|
60
65
|
```
|
|
61
66
|
|