gitstandup-mcp 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 GitStandup Contributors
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,205 @@
1
+ # GitStandup MCP Server
2
+
3
+ > Generate daily standup notes from your git commits using AI
4
+
5
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that automatically collects your git commits from multiple repositories and helps AI assistants generate natural, comprehensive standup summaries.
6
+
7
+ ## โœจ Features
8
+
9
+ - ๐Ÿ“ฆ **Multi-repo support** - Track commits across all your projects
10
+ - ๐Ÿ‘ค **User-specific** - Only shows your commits (filtered by git user.email)
11
+ - โฐ **Time-based** - Configurable lookback period (default: last 24 hours)
12
+ - ๐ŸŽฏ **Smart diff analysis** - Includes code changes with intelligent truncation
13
+ - ๐Ÿ’พ **Persistent config** - Remembers your repos in `~/.gitstandup/config.json`
14
+ - ๐Ÿงน **Clean output** - Skips generated files (lock files, minified code)
15
+
16
+ ## ๐Ÿš€ Quick Start
17
+
18
+ ### Installation
19
+
20
+ ```bash
21
+ # Using npx (no installation needed)
22
+ npx -y gitstandup-mcp
23
+
24
+ # Or install globally
25
+ npm install -g gitstandup-mcp
26
+ ```
27
+
28
+ ### Setup with Claude Desktop
29
+
30
+ Add to your Claude Desktop config at `~/Library/Application Support/Claude/claude_desktop_config.json`:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "gitstandup": {
36
+ "command": "npx",
37
+ "args": ["-y", "gitstandup-mcp"]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### Setup with VS Code (GitHub Copilot)
44
+
45
+ Add to your VS Code MCP settings:
46
+
47
+ ```json
48
+ {
49
+ "gitstandup": {
50
+ "type": "stdio",
51
+ "command": "npx",
52
+ "args": ["-y", "gitstandup-mcp"]
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## ๐Ÿ“– Usage
58
+
59
+ Once configured, you can use natural language with your AI assistant:
60
+
61
+ ```
62
+ "Generate my standup notes"
63
+ "What did I work on yesterday?"
64
+ "Show my commits from the last 2 days"
65
+ ```
66
+
67
+ ### First Time Setup
68
+
69
+ 1. **Add your repositories:**
70
+
71
+ ```
72
+ "Add /path/to/my/project to GitStandup"
73
+ ```
74
+
75
+ 2. **Generate standup notes:**
76
+
77
+ ```
78
+ "Generate my standup notes"
79
+ ```
80
+
81
+ 3. **The AI will create a summary like:**
82
+ > Yesterday I:
83
+ >
84
+ > - Implemented OAuth authentication flow in the api-server
85
+ > - Fixed critical bug in payment processing
86
+ > - Added integration tests for user registration
87
+
88
+ ## ๐Ÿ› ๏ธ Available Tools
89
+
90
+ The server exposes four MCP tools that AI assistants can use:
91
+
92
+ ### `generate_standup`
93
+
94
+ Generate standup notes from configured repositories.
95
+
96
+ **Parameters:**
97
+
98
+ - `hours` (optional): Number of hours to look back (default: 24)
99
+ - `repos` (optional): Array of specific repo paths to use
100
+
101
+ **Example:**
102
+
103
+ ```typescript
104
+ {
105
+ "hours": 48, // Last 2 days
106
+ "repos": ["/path/to/repo1", "/path/to/repo2"] // Optional
107
+ }
108
+ ```
109
+
110
+ ### `add_repos`
111
+
112
+ Add repository paths to the configuration.
113
+
114
+ **Parameters:**
115
+
116
+ - `paths`: Array of absolute paths to git repositories
117
+
118
+ **Example:**
119
+
120
+ ```typescript
121
+ {
122
+ "paths": ["/Users/you/projects/my-app", "/Users/you/projects/api"]
123
+ }
124
+ ```
125
+
126
+ ### `list_repos`
127
+
128
+ List currently configured repositories.
129
+
130
+ **Returns:** Array of configured repository paths
131
+
132
+ ### `remove_repos`
133
+
134
+ Remove repository paths from the configuration.
135
+
136
+ **Parameters:**
137
+
138
+ - `paths`: Array of repository paths to remove
139
+
140
+ ## ๐Ÿ”ง Development
141
+
142
+ ```bash
143
+ # Clone the repository
144
+ git clone https://github.com/muba00/gitstandup.git
145
+ cd gitstandup
146
+
147
+ # Install dependencies
148
+ npm install
149
+
150
+ # Build
151
+ npm run build
152
+
153
+ # Test locally
154
+ node build/index.js
155
+ ```
156
+
157
+ ### Project Structure
158
+
159
+ ```
160
+ gitstandup/
161
+ โ”œโ”€โ”€ src/
162
+ โ”‚ โ”œโ”€โ”€ index.ts # MCP server setup and tool definitions
163
+ โ”‚ โ”œโ”€โ”€ git.ts # Git operations and commit collection
164
+ โ”‚ โ””โ”€โ”€ config.ts # Configuration management
165
+ โ”œโ”€โ”€ build/ # Compiled JavaScript (generated)
166
+ โ””โ”€โ”€ package.json
167
+ ```
168
+
169
+ ## ๐Ÿ“ Configuration
170
+
171
+ Repository paths are stored in `~/.gitstandup/config.json`:
172
+
173
+ ```json
174
+ {
175
+ "repos": ["/Users/you/projects/project1", "/Users/you/projects/project2"]
176
+ }
177
+ ```
178
+
179
+ You can edit this file manually or use the `add_repos` and `remove_repos` tools.
180
+
181
+ ## ๐Ÿค Contributing
182
+
183
+ Contributions are welcome! Feel free to:
184
+
185
+ - ๐Ÿ› Report bugs
186
+ - ๐Ÿ’ก Suggest new features
187
+ - ๐Ÿ”ง Submit pull requests
188
+
189
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for details.
190
+
191
+ ## ๐Ÿ“„ License
192
+
193
+ MIT License - see [LICENSE](LICENSE) for details
194
+
195
+ ## ๐Ÿ™ Acknowledgments
196
+
197
+ Built with:
198
+
199
+ - [Model Context Protocol SDK](https://github.com/modelcontextprotocol/typescript-sdk)
200
+ - [simple-git](https://github.com/steveukx/git-js)
201
+ - [Zod](https://github.com/colinhacks/zod)
202
+
203
+ ---
204
+
205
+ **Note:** This tool only reads git commit history and does not modify your repositories.
@@ -0,0 +1,34 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ const CONFIG_DIR = path.join(os.homedir(), ".gitstandup");
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
+ /**
8
+ * Ensure the config directory exists
9
+ */
10
+ export async function ensureConfigDir() {
11
+ if (!existsSync(CONFIG_DIR)) {
12
+ await mkdir(CONFIG_DIR, { recursive: true });
13
+ }
14
+ }
15
+ /**
16
+ * Load configuration from ~/.gitstandup/config.json
17
+ */
18
+ export async function loadConfig() {
19
+ try {
20
+ const data = await readFile(CONFIG_FILE, "utf-8");
21
+ return JSON.parse(data);
22
+ }
23
+ catch (error) {
24
+ // If file doesn't exist or is invalid, return empty config
25
+ return { repos: [] };
26
+ }
27
+ }
28
+ /**
29
+ * Save configuration to ~/.gitstandup/config.json
30
+ */
31
+ export async function saveConfig(config) {
32
+ await ensureConfigDir();
33
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
34
+ }
package/build/git.js ADDED
@@ -0,0 +1,134 @@
1
+ import { simpleGit } from "simple-git";
2
+ import path from "path";
3
+ const MAX_DIFF_LINES_PER_FILE = 500;
4
+ const MAX_DIFF_LINES_PER_COMMIT = 2000;
5
+ /**
6
+ * Truncate a diff to stay within limits
7
+ */
8
+ function truncateDiff(diff, maxLines) {
9
+ const lines = diff.split("\n");
10
+ if (lines.length <= maxLines) {
11
+ return diff;
12
+ }
13
+ const halfLines = Math.floor(maxLines / 2);
14
+ const start = lines.slice(0, halfLines);
15
+ const end = lines.slice(-halfLines);
16
+ return [
17
+ ...start,
18
+ `\n... (${lines.length - maxLines} lines omitted) ...\n`,
19
+ ...end,
20
+ ].join("\n");
21
+ }
22
+ /**
23
+ * Get the current git user email for a repository
24
+ */
25
+ async function getCurrentUserEmail(git) {
26
+ try {
27
+ const email = await git.raw(["config", "user.email"]);
28
+ return email.trim();
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ /**
35
+ * Collect commits from a single repository
36
+ */
37
+ async function collectRepoCommits(repoPath, hoursAgo) {
38
+ const repoName = path.basename(repoPath);
39
+ try {
40
+ const git = simpleGit(repoPath);
41
+ // Check if it's a valid git repository
42
+ const isRepo = await git.checkIsRepo();
43
+ if (!isRepo) {
44
+ return {
45
+ name: repoName,
46
+ path: repoPath,
47
+ error: "Not a git repository",
48
+ };
49
+ }
50
+ // Get current user email
51
+ const userEmail = await getCurrentUserEmail(git);
52
+ if (!userEmail) {
53
+ return {
54
+ name: repoName,
55
+ path: repoPath,
56
+ error: "Could not determine git user.email",
57
+ };
58
+ }
59
+ // Calculate time threshold
60
+ const since = new Date(Date.now() - hoursAgo * 60 * 60 * 1000);
61
+ const sinceStr = since.toISOString();
62
+ // Get commits by current user since the time threshold
63
+ const log = await git.log({
64
+ "--since": sinceStr,
65
+ "--author": userEmail,
66
+ "--all": null,
67
+ });
68
+ if (log.all.length === 0) {
69
+ return {
70
+ name: repoName,
71
+ path: repoPath,
72
+ commits: [],
73
+ };
74
+ }
75
+ // Collect commit details with diffs
76
+ const commits = [];
77
+ for (const commit of log.all) {
78
+ try {
79
+ // Get the diff for this commit
80
+ const diffSummary = await git.diffSummary([
81
+ `${commit.hash}^`,
82
+ commit.hash,
83
+ ]);
84
+ // Get full diff, but limit to avoid huge output
85
+ let fullDiff = await git.diff([`${commit.hash}^`, commit.hash]);
86
+ // Truncate diff if too large
87
+ fullDiff = truncateDiff(fullDiff, MAX_DIFF_LINES_PER_COMMIT);
88
+ // Skip generated files and lock files
89
+ const relevantFiles = diffSummary.files.filter((f) => {
90
+ const fileName = f.file.toLowerCase();
91
+ return (!fileName.includes("package-lock.json") &&
92
+ !fileName.includes("yarn.lock") &&
93
+ !fileName.includes(".min.") &&
94
+ !fileName.endsWith(".lock"));
95
+ });
96
+ commits.push({
97
+ hash: commit.hash.substring(0, 7),
98
+ message: commit.message,
99
+ author: userEmail,
100
+ timestamp: commit.date,
101
+ files: relevantFiles.map((f) => f.file),
102
+ diff: fullDiff,
103
+ stats: {
104
+ additions: diffSummary.insertions,
105
+ deletions: diffSummary.deletions,
106
+ },
107
+ });
108
+ }
109
+ catch (error) {
110
+ // If we can't get diff for a commit, skip it
111
+ console.error(`Error getting diff for commit ${commit.hash}:`, error);
112
+ }
113
+ }
114
+ return {
115
+ name: repoName,
116
+ path: repoPath,
117
+ commits,
118
+ };
119
+ }
120
+ catch (error) {
121
+ return {
122
+ name: repoName,
123
+ path: repoPath,
124
+ error: error instanceof Error ? error.message : "Unknown error",
125
+ };
126
+ }
127
+ }
128
+ /**
129
+ * Collect commits from multiple repositories
130
+ */
131
+ export async function collectCommits(repoPaths, hoursAgo) {
132
+ const results = await Promise.all(repoPaths.map((path) => collectRepoCommits(path, hoursAgo)));
133
+ return results;
134
+ }
package/build/index.js ADDED
@@ -0,0 +1,185 @@
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 { collectCommits } from "./git.js";
6
+ import { loadConfig, saveConfig, ensureConfigDir } from "./config.js";
7
+ const server = new McpServer({
8
+ name: "gitstandup-mcp",
9
+ version: "1.0.0",
10
+ });
11
+ // Tool: generate_standup
12
+ server.registerTool("generate_standup", {
13
+ title: "Generate Standup Notes",
14
+ description: "Generate standup notes from git commits in configured repositories",
15
+ inputSchema: {
16
+ hours: z.number().optional().describe("Hours to look back (default: 24)"),
17
+ repos: z
18
+ .array(z.string())
19
+ .optional()
20
+ .describe("Specific repo paths to use instead of configured ones"),
21
+ },
22
+ outputSchema: {
23
+ repos: z.array(z.object({
24
+ name: z.string(),
25
+ path: z.string(),
26
+ commits: z.array(z.object({
27
+ hash: z.string(),
28
+ message: z.string(),
29
+ author: z.string(),
30
+ timestamp: z.string(),
31
+ files: z.array(z.string()),
32
+ diff: z.string(),
33
+ stats: z.object({
34
+ additions: z.number(),
35
+ deletions: z.number(),
36
+ }),
37
+ })),
38
+ error: z.string().optional(),
39
+ })),
40
+ summary: z.object({
41
+ totalCommits: z.number(),
42
+ totalRepos: z.number(),
43
+ timeRange: z.string(),
44
+ user: z.string().optional(),
45
+ }),
46
+ },
47
+ }, async ({ hours = 24, repos }) => {
48
+ const config = await loadConfig();
49
+ const repoPaths = repos || config.repos;
50
+ if (repoPaths.length === 0) {
51
+ const output = {
52
+ repos: [],
53
+ summary: {
54
+ totalCommits: 0,
55
+ totalRepos: 0,
56
+ timeRange: `last ${hours} hours`,
57
+ },
58
+ };
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: "No repositories configured. Use add_repos tool to add repositories.",
64
+ },
65
+ ],
66
+ structuredContent: output,
67
+ };
68
+ }
69
+ const results = await collectCommits(repoPaths, hours);
70
+ const totalCommits = results.reduce((sum, r) => sum + (r.commits?.length || 0), 0);
71
+ const output = {
72
+ repos: results,
73
+ summary: {
74
+ totalCommits,
75
+ totalRepos: results.length,
76
+ timeRange: `last ${hours} hours`,
77
+ user: results[0]?.commits?.[0]?.author,
78
+ },
79
+ };
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: JSON.stringify(output, null, 2),
85
+ },
86
+ ],
87
+ structuredContent: output,
88
+ };
89
+ });
90
+ // Tool: add_repos
91
+ server.registerTool("add_repos", {
92
+ title: "Add Repositories",
93
+ description: "Add repository paths to the configuration",
94
+ inputSchema: {
95
+ paths: z.array(z.string()).describe("Absolute paths to git repositories"),
96
+ },
97
+ outputSchema: {
98
+ added: z.array(z.string()),
99
+ repos: z.array(z.string()),
100
+ },
101
+ }, async ({ paths }) => {
102
+ await ensureConfigDir();
103
+ const config = await loadConfig();
104
+ const newPaths = paths.filter((p) => !config.repos.includes(p));
105
+ config.repos.push(...newPaths);
106
+ await saveConfig(config);
107
+ const output = {
108
+ added: newPaths,
109
+ repos: config.repos,
110
+ };
111
+ return {
112
+ content: [
113
+ {
114
+ type: "text",
115
+ text: `Added ${newPaths.length} repository(ies). Total: ${config.repos.length}`,
116
+ },
117
+ ],
118
+ structuredContent: output,
119
+ };
120
+ });
121
+ // Tool: list_repos
122
+ server.registerTool("list_repos", {
123
+ title: "List Repositories",
124
+ description: "List currently configured repositories",
125
+ inputSchema: {},
126
+ outputSchema: {
127
+ repos: z.array(z.string()),
128
+ count: z.number(),
129
+ },
130
+ }, async () => {
131
+ const config = await loadConfig();
132
+ const output = {
133
+ repos: config.repos,
134
+ count: config.repos.length,
135
+ };
136
+ return {
137
+ content: [
138
+ {
139
+ type: "text",
140
+ text: JSON.stringify(output, null, 2),
141
+ },
142
+ ],
143
+ structuredContent: output,
144
+ };
145
+ });
146
+ // Tool: remove_repos
147
+ server.registerTool("remove_repos", {
148
+ title: "Remove Repositories",
149
+ description: "Remove repository paths from the configuration",
150
+ inputSchema: {
151
+ paths: z.array(z.string()).describe("Repository paths to remove"),
152
+ },
153
+ outputSchema: {
154
+ removed: z.array(z.string()),
155
+ repos: z.array(z.string()),
156
+ },
157
+ }, async ({ paths }) => {
158
+ const config = await loadConfig();
159
+ const removed = paths.filter((p) => config.repos.includes(p));
160
+ config.repos = config.repos.filter((p) => !paths.includes(p));
161
+ await saveConfig(config);
162
+ const output = {
163
+ removed,
164
+ repos: config.repos,
165
+ };
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `Removed ${removed.length} repository(ies). Remaining: ${config.repos.length}`,
171
+ },
172
+ ],
173
+ structuredContent: output,
174
+ };
175
+ });
176
+ // Start the server
177
+ async function main() {
178
+ const transport = new StdioServerTransport();
179
+ await server.connect(transport);
180
+ console.error("GitStandup MCP Server running on stdio");
181
+ }
182
+ main().catch((error) => {
183
+ console.error("Fatal error:", error);
184
+ process.exit(1);
185
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "gitstandup-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for generating daily standup notes from git commits",
5
+ "type": "module",
6
+ "bin": {
7
+ "gitstandup-mcp": "./build/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && chmod +x build/index.js",
11
+ "dev": "tsc --watch",
12
+ "prepare": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "git",
17
+ "standup",
18
+ "commits"
19
+ ],
20
+ "author": "",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.0.0",
24
+ "simple-git": "^3.25.0",
25
+ "zod": "^3.23.8"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.0.0",
29
+ "typescript": "^5.3.0"
30
+ },
31
+ "files": [
32
+ "build"
33
+ ]
34
+ }