standup-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 +234 -0
- package/dist/analytics/blockers.js +122 -0
- package/dist/analytics/digest.js +35 -0
- package/dist/analytics/draft.js +129 -0
- package/dist/analytics/grouping.js +44 -0
- package/dist/analytics/weekly.js +42 -0
- package/dist/config.js +67 -0
- package/dist/demo.js +93 -0
- package/dist/format.js +66 -0
- package/dist/index.js +63 -0
- package/dist/normalize.js +101 -0
- package/dist/provider.js +76 -0
- package/dist/sources/github.js +192 -0
- package/dist/sources/jira.js +192 -0
- package/dist/sources/linear.js +176 -0
- package/dist/sources/mock.js +257 -0
- package/dist/sources/slack.js +185 -0
- package/dist/tools/blockers.js +20 -0
- package/dist/tools/digest.js +16 -0
- package/dist/tools/registry.js +13 -0
- package/dist/tools/sources.js +29 -0
- package/dist/tools/standup.js +26 -0
- package/dist/tools/util.js +35 -0
- package/dist/tools/weekly.js +25 -0
- package/dist/types.js +10 -0
- package/dist/window.js +62 -0
- package/package.json +61 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Slack source. Implements ActivitySource against the Slack Web API
|
|
3
|
+
* (https://slack.com/api) using a bearer token. Read-only: it only reads the
|
|
4
|
+
* user's own messages to build "message" activity events.
|
|
5
|
+
*
|
|
6
|
+
* Slack returns HTTP 200 with { ok: false, error } on logical failure, so a
|
|
7
|
+
* successful HTTP status is not enough. Only checkConnection surfaces failure;
|
|
8
|
+
* fetchActivity degrades to [] because Slack token scopes vary widely (a bot
|
|
9
|
+
* token cannot use search.messages, a user token may lack channel access, etc).
|
|
10
|
+
*/
|
|
11
|
+
import { detectTextSignals, extractWorkItemKey, truncate } from "../normalize.js";
|
|
12
|
+
const API_BASE = "https://slack.com/api";
|
|
13
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
14
|
+
export class SlackSource {
|
|
15
|
+
creds;
|
|
16
|
+
id = "slack";
|
|
17
|
+
isDemo = false;
|
|
18
|
+
constructor(creds) {
|
|
19
|
+
this.creds = creds;
|
|
20
|
+
}
|
|
21
|
+
// --- HTTP plumbing -------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* GET a Web API method and parse JSON. Throws on transport failure, timeout,
|
|
24
|
+
* or a logical Slack error ({ ok: false }). Callers that must not fail wrap
|
|
25
|
+
* this in their own try/catch.
|
|
26
|
+
*/
|
|
27
|
+
async call(method, params = {}) {
|
|
28
|
+
const query = new URLSearchParams(params).toString();
|
|
29
|
+
const url = `${API_BASE}/${method}${query ? `?${query}` : ""}`;
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(url, {
|
|
34
|
+
method: "GET",
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${this.creds.token}`,
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
},
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`Slack API ${res.status} ${res.statusText} for ${method}`);
|
|
43
|
+
}
|
|
44
|
+
const data = (await res.json());
|
|
45
|
+
if (!data.ok) {
|
|
46
|
+
throw new Error(data.error ?? "unknown_error");
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
52
|
+
throw new Error(`Slack API request timed out after 30s: ${method}`);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// --- ActivitySource surface ----------------------------------------------
|
|
61
|
+
async resolveIdentity() {
|
|
62
|
+
try {
|
|
63
|
+
const data = await this.call("auth.test");
|
|
64
|
+
return data.user_id;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async checkConnection() {
|
|
71
|
+
try {
|
|
72
|
+
const data = await this.call("auth.test");
|
|
73
|
+
return {
|
|
74
|
+
source: "slack",
|
|
75
|
+
ok: data.ok === true,
|
|
76
|
+
identity: data.user_id,
|
|
77
|
+
detail: `authenticated as ${data.user} in ${data.team}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
82
|
+
return {
|
|
83
|
+
source: "slack",
|
|
84
|
+
ok: false,
|
|
85
|
+
detail: `Slack error: ${reason}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The user's own messages in the window, as "message" events. Never throws:
|
|
91
|
+
* tries search.messages first, falls back to scanning configured channels via
|
|
92
|
+
* conversations.history, and returns [] if both yield nothing or fail.
|
|
93
|
+
*/
|
|
94
|
+
async fetchActivity(window, actor) {
|
|
95
|
+
try {
|
|
96
|
+
const userId = actor.slack ?? (await this.resolveIdentity());
|
|
97
|
+
if (!userId)
|
|
98
|
+
return [];
|
|
99
|
+
// Strategy 1: search.messages. Needs a user token with search:read.
|
|
100
|
+
try {
|
|
101
|
+
const events = await this.searchMessages(userId, window);
|
|
102
|
+
// Empty matches is a valid result, not a failure: do not fall through.
|
|
103
|
+
return events;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Fall through to channel scanning below.
|
|
107
|
+
}
|
|
108
|
+
// Strategy 2: conversations.history per configured channel.
|
|
109
|
+
if (this.creds.channels?.length) {
|
|
110
|
+
return await this.scanChannels(userId, window);
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Slack has no work items, so nothing implies upcoming work here. */
|
|
119
|
+
async fetchOpenItems(_actor) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
// --- Strategies ----------------------------------------------------------
|
|
123
|
+
async searchMessages(userId, window) {
|
|
124
|
+
const dayBefore = new Date(Date.parse(window.since) - 86_400_000)
|
|
125
|
+
.toISOString()
|
|
126
|
+
.slice(0, 10);
|
|
127
|
+
const data = await this.call("search.messages", {
|
|
128
|
+
query: `from:<@${userId}> after:${dayBefore}`,
|
|
129
|
+
count: "100",
|
|
130
|
+
});
|
|
131
|
+
const matches = data.messages?.matches ?? [];
|
|
132
|
+
return matches
|
|
133
|
+
.map((m) => this.toEvent(m, userId, m.channel?.name))
|
|
134
|
+
.filter((e) => e !== null && this.inWindow(e, window));
|
|
135
|
+
}
|
|
136
|
+
async scanChannels(userId, window) {
|
|
137
|
+
const oldest = String(Math.floor(Date.parse(window.since) / 1000));
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const channelId of this.creds.channels ?? []) {
|
|
140
|
+
try {
|
|
141
|
+
const data = await this.call("conversations.history", {
|
|
142
|
+
channel: channelId,
|
|
143
|
+
oldest,
|
|
144
|
+
limit: "100",
|
|
145
|
+
});
|
|
146
|
+
for (const msg of data.messages ?? []) {
|
|
147
|
+
if (msg.user !== userId)
|
|
148
|
+
continue;
|
|
149
|
+
// conversations.history returns only the channel id, no name.
|
|
150
|
+
const event = this.toEvent(msg, userId, channelId);
|
|
151
|
+
if (event && this.inWindow(event, window))
|
|
152
|
+
out.push(event);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// A single channel can fail (not_in_channel, etc); skip it and continue.
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
// --- Mapping -------------------------------------------------------------
|
|
163
|
+
/** Map a raw Slack message to a normalized "message" event, or null if empty. */
|
|
164
|
+
toEvent(msg, userId, channelName) {
|
|
165
|
+
if (!msg.ts)
|
|
166
|
+
return null;
|
|
167
|
+
const text = msg.text ?? "";
|
|
168
|
+
const timestamp = new Date(Math.floor(Number(msg.ts)) * 1000).toISOString();
|
|
169
|
+
return {
|
|
170
|
+
source: "slack",
|
|
171
|
+
kind: "message",
|
|
172
|
+
timestamp,
|
|
173
|
+
actor: userId,
|
|
174
|
+
title: `#${channelName ?? "dm"}: ${truncate(text, 80)}`,
|
|
175
|
+
body: text,
|
|
176
|
+
workItem: extractWorkItemKey(text),
|
|
177
|
+
signals: detectTextSignals(text),
|
|
178
|
+
...(msg.permalink ? { url: msg.permalink } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Keep only messages at or after the window start (spec filters on `since` only). */
|
|
182
|
+
inWindow(event, window) {
|
|
183
|
+
return Date.parse(event.timestamp) >= Date.parse(window.since);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { detectBlockers, formatBlockerReport } from "../analytics/blockers.js";
|
|
2
|
+
import { resolveWindow } from "../window.js";
|
|
3
|
+
import { handle, windowArgs } from "./util.js";
|
|
4
|
+
export function registerBlockerScan(server, deps) {
|
|
5
|
+
server.registerTool("blocker_scan", {
|
|
6
|
+
title: "Blocker Scan",
|
|
7
|
+
description: "Surface what is blocking you across GitHub, Jira, Linear, and Slack, ranked by severity. Detects blockers from signals, not self-report: explicit 'blocked / waiting on / stuck' language, PRs awaiting review, in-progress tickets that have not moved, and requests for help.",
|
|
8
|
+
inputSchema: { ...windowArgs },
|
|
9
|
+
}, async ({ since, days }) => handle(async () => {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const window = resolveWindow({ since, days }, now);
|
|
12
|
+
const actor = await deps.provider.resolveActor();
|
|
13
|
+
const [events, openItems] = await Promise.all([
|
|
14
|
+
deps.provider.fetchActivity(window, actor),
|
|
15
|
+
deps.provider.fetchOpenItems(actor),
|
|
16
|
+
]);
|
|
17
|
+
const blockers = detectBlockers(events, openItems, now.toISOString());
|
|
18
|
+
return formatBlockerReport(blockers, window, deps.provider.isDemo);
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { buildDigest } from "../analytics/digest.js";
|
|
2
|
+
import { resolveWindow } from "../window.js";
|
|
3
|
+
import { handle, windowArgs } from "./util.js";
|
|
4
|
+
export function registerActivityDigest(server, deps) {
|
|
5
|
+
server.registerTool("activity_digest", {
|
|
6
|
+
title: "Activity Digest",
|
|
7
|
+
description: "A chronological digest of everything you did in a window across the configured tools, newest first, grouped by day. Use it to reconstruct what actually happened, separate from the opinionated standup framing.",
|
|
8
|
+
inputSchema: { ...windowArgs },
|
|
9
|
+
}, async ({ since, days }) => handle(async () => {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const window = resolveWindow({ since, days }, now);
|
|
12
|
+
const actor = await deps.provider.resolveActor();
|
|
13
|
+
const events = await deps.provider.fetchActivity(window, actor);
|
|
14
|
+
return buildDigest(events, window, deps.provider.isDemo);
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { registerStandupDraft } from "./standup.js";
|
|
2
|
+
import { registerBlockerScan } from "./blockers.js";
|
|
3
|
+
import { registerWeeklySummary } from "./weekly.js";
|
|
4
|
+
import { registerActivityDigest } from "./digest.js";
|
|
5
|
+
import { registerListSources } from "./sources.js";
|
|
6
|
+
/** Wire every tool onto the server. One place to see the full tool surface. */
|
|
7
|
+
export function registerAllTools(server, deps) {
|
|
8
|
+
registerStandupDraft(server, deps);
|
|
9
|
+
registerBlockerScan(server, deps);
|
|
10
|
+
registerWeeklySummary(server, deps);
|
|
11
|
+
registerActivityDigest(server, deps);
|
|
12
|
+
registerListSources(server, deps);
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { sourceLabel } from "../format.js";
|
|
2
|
+
import { handle } from "./util.js";
|
|
3
|
+
export function registerListSources(server, deps) {
|
|
4
|
+
server.registerTool("list_sources", {
|
|
5
|
+
title: "List Sources",
|
|
6
|
+
description: "Show which activity sources (GitHub, Jira, Linear, Slack) are configured, or that the server is running on demo data. Does not hit the network; run the server with --check to verify live connections.",
|
|
7
|
+
inputSchema: {},
|
|
8
|
+
}, async () => handle(async () => {
|
|
9
|
+
const p = deps.provider;
|
|
10
|
+
if (p.isDemo) {
|
|
11
|
+
return [
|
|
12
|
+
"Running in **demo mode** (synthetic data, no credentials configured).",
|
|
13
|
+
"",
|
|
14
|
+
"To use your real activity, set any of these and restart:",
|
|
15
|
+
"- GitHub: GITHUB_TOKEN",
|
|
16
|
+
"- Jira: JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN",
|
|
17
|
+
"- Linear: LINEAR_API_KEY",
|
|
18
|
+
"- Slack: SLACK_TOKEN",
|
|
19
|
+
"",
|
|
20
|
+
"Then run `standup-mcp --check` to verify each connection.",
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
const lines = ["**Configured sources:**", ""];
|
|
24
|
+
for (const s of p.sources)
|
|
25
|
+
lines.push(`- ${sourceLabel(s)}`);
|
|
26
|
+
lines.push("", "Run `standup-mcp --check` to verify each connection live.");
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildStandupDraft } from "../analytics/draft.js";
|
|
2
|
+
import { resolveWindow } from "../window.js";
|
|
3
|
+
import { handle, windowArgs } from "./util.js";
|
|
4
|
+
export function registerStandupDraft(server, deps) {
|
|
5
|
+
server.registerTool("standup_draft", {
|
|
6
|
+
title: "Standup Draft",
|
|
7
|
+
description: "Generate your daily standup, what should I say at standup today, from your real activity across the configured tools (GitHub, Jira, Linear, Slack). Returns Yesterday / Today / Blockers grouped by work item, not by tool, in concrete deltas. Defaults to the last working day (Monday reaches back across the weekend). Only reports observed activity; never invents work.",
|
|
8
|
+
inputSchema: { ...windowArgs },
|
|
9
|
+
}, async ({ since, days }) => handle(async () => {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const window = resolveWindow({ since, days }, now);
|
|
12
|
+
const actor = await deps.provider.resolveActor();
|
|
13
|
+
const [events, openItems] = await Promise.all([
|
|
14
|
+
deps.provider.fetchActivity(window, actor),
|
|
15
|
+
deps.provider.fetchOpenItems(actor),
|
|
16
|
+
]);
|
|
17
|
+
return buildStandupDraft({
|
|
18
|
+
actor,
|
|
19
|
+
window,
|
|
20
|
+
events,
|
|
21
|
+
openItems,
|
|
22
|
+
nowIso: now.toISOString(),
|
|
23
|
+
isDemo: deps.provider.isDemo,
|
|
24
|
+
});
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for tool handlers: dependency injection, consistent result
|
|
3
|
+
* shaping, friendly error handling, and the window-selection input schema every
|
|
4
|
+
* activity tool accepts.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
export function text(s) {
|
|
8
|
+
return { content: [{ type: "text", text: s }] };
|
|
9
|
+
}
|
|
10
|
+
/** Run a handler, turning any thrown error into a clean tool error result. */
|
|
11
|
+
export async function handle(fn) {
|
|
12
|
+
try {
|
|
13
|
+
return text(await fn());
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: `⚠️ ${message}` }],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Input shared by the activity tools: choose the time window. */
|
|
24
|
+
export const windowArgs = {
|
|
25
|
+
since: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("ISO date/time start of the window, e.g. 2026-06-16. Overrides the default. Default covers the last working day (Monday reaches back to Friday)."),
|
|
29
|
+
days: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.positive()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Look back this many days from now instead of the default window."),
|
|
35
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { buildWeeklySummary } from "../analytics/weekly.js";
|
|
3
|
+
import { lastNDaysWindow } from "../window.js";
|
|
4
|
+
import { handle } from "./util.js";
|
|
5
|
+
export function registerWeeklySummary(server, deps) {
|
|
6
|
+
server.registerTool("weekly_summary", {
|
|
7
|
+
title: "Weekly Summary",
|
|
8
|
+
description: "Roll up a week (or any N days) of your real activity into accomplishments, grouped by work item as Shipped vs In progress, with reviews and totals. For 1:1s, weekly status, and self-reviews. Only reports observed work.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
days: z
|
|
11
|
+
.number()
|
|
12
|
+
.int()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(31)
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("How many days to summarize. Default 7."),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ days }) => handle(async () => {
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const window = lastNDaysWindow(days ?? 7, now);
|
|
21
|
+
const actor = await deps.provider.resolveActor();
|
|
22
|
+
const events = await deps.provider.fetchActivity(window, actor);
|
|
23
|
+
return buildWeeklySummary(events, window, actor, deps.provider.isDemo);
|
|
24
|
+
}));
|
|
25
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized activity types.
|
|
3
|
+
*
|
|
4
|
+
* Everything downstream of a source (grouping, blocker detection, the standup
|
|
5
|
+
* draft) speaks these types only. Each live source client (GitHub, Jira, Linear,
|
|
6
|
+
* Slack) and the demo provider all emit ActivityEvent, so no analytics code
|
|
7
|
+
* knows or cares which tool the activity came from. That is what lets us group
|
|
8
|
+
* by work item across tools and run the whole engine on synthetic demo data.
|
|
9
|
+
*/
|
|
10
|
+
export const ALL_SOURCES = ["github", "jira", "linear", "slack"];
|
package/dist/window.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the time window a standup or digest covers.
|
|
3
|
+
*
|
|
4
|
+
* The default answers the real question, "what happened since my last
|
|
5
|
+
* standup?", which means Monday has to reach back across the weekend to Friday.
|
|
6
|
+
* Kept pure (takes `now`) so the weekend logic is unit tested.
|
|
7
|
+
*
|
|
8
|
+
* All math is in UTC for determinism. Pass an explicit `since` for a precise
|
|
9
|
+
* local window.
|
|
10
|
+
*/
|
|
11
|
+
import { fmtDayDate } from "./format.js";
|
|
12
|
+
const DAY_MS = 86_400_000;
|
|
13
|
+
function startOfUTCDay(d) {
|
|
14
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How many days back the default window reaches, so the previous working day
|
|
18
|
+
* (and any weekend in between) is always covered.
|
|
19
|
+
* Mon -> 3 (Fri + Sat + Sun) Sun -> 2 (Fri + Sat) Sat -> 1 (Fri)
|
|
20
|
+
* Tue..Fri -> 1 (the day before)
|
|
21
|
+
*/
|
|
22
|
+
export function defaultLookbackDays(now) {
|
|
23
|
+
switch (now.getUTCDay()) {
|
|
24
|
+
case 1: // Monday
|
|
25
|
+
return 3;
|
|
26
|
+
case 0: // Sunday
|
|
27
|
+
return 2;
|
|
28
|
+
default:
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function resolveWindow(opts, now) {
|
|
33
|
+
const until = now.toISOString();
|
|
34
|
+
let since;
|
|
35
|
+
if (opts.since) {
|
|
36
|
+
since = new Date(opts.since).toISOString();
|
|
37
|
+
}
|
|
38
|
+
else if (opts.days && opts.days > 0) {
|
|
39
|
+
since = new Date(now.getTime() - opts.days * DAY_MS).toISOString();
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const back = defaultLookbackDays(now);
|
|
43
|
+
since = startOfUTCDay(new Date(now.getTime() - back * DAY_MS)).toISOString();
|
|
44
|
+
}
|
|
45
|
+
return { since, until, label: `since ${fmtDayDate(since)}` };
|
|
46
|
+
}
|
|
47
|
+
/** A window covering the last `days` days, for weekly summaries. */
|
|
48
|
+
export function lastNDaysWindow(days, now) {
|
|
49
|
+
const since = startOfUTCDay(new Date(now.getTime() - (days - 1) * DAY_MS)).toISOString();
|
|
50
|
+
return {
|
|
51
|
+
since,
|
|
52
|
+
until: now.toISOString(),
|
|
53
|
+
label: `the last ${days} days`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** True if an ISO timestamp falls within [since, until). */
|
|
57
|
+
export function inWindow(iso, window) {
|
|
58
|
+
const t = Date.parse(iso);
|
|
59
|
+
if (Number.isNaN(t))
|
|
60
|
+
return false;
|
|
61
|
+
return t >= Date.parse(window.since) && t < Date.parse(window.until);
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "standup-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.sathvic-kollu/standup-mcp",
|
|
5
|
+
"description": "Generate your daily standup from real activity across GitHub, Jira, Linear, and Slack. An MCP server that answers \"what should I say at standup today?\" from yesterday's commits, PRs, ticket moves, and threads. Local, read-only, model-agnostic.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"standup-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && chmod +x dist/index.js",
|
|
18
|
+
"dev": "tsx watch src/index.ts",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"smoke": "tsx test/smoke.ts",
|
|
21
|
+
"test": "tsx test/normalize.test.ts && tsx test/draft.test.ts && npm run build && tsx test/integration.ts",
|
|
22
|
+
"inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"standup",
|
|
29
|
+
"daily-standup",
|
|
30
|
+
"scrum",
|
|
31
|
+
"github",
|
|
32
|
+
"jira",
|
|
33
|
+
"linear",
|
|
34
|
+
"slack",
|
|
35
|
+
"developer-tools",
|
|
36
|
+
"claude",
|
|
37
|
+
"ai"
|
|
38
|
+
],
|
|
39
|
+
"author": "Sathvic Kollu <sathvickollu@gmail.com> (https://sathvickollu.com)",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"homepage": "https://github.com/sathvic-kollu/standup-mcp#readme",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/sathvic-kollu/standup-mcp.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/sathvic-kollu/standup-mcp/issues"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
54
|
+
"zod": "^3.23.8"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"tsx": "^4.19.0",
|
|
59
|
+
"typescript": "^5.6.0"
|
|
60
|
+
}
|
|
61
|
+
}
|