git-mob-mcp-server 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 +21 -0
- package/README.md +89 -0
- package/dist/clients/gitMobClient.js +38 -0
- package/dist/clients/gitMobClient.test.js +114 -0
- package/dist/gitMobServerFactory.js +34 -0
- package/dist/gitMobServerFactory.test.js +58 -0
- package/dist/helpers/index.js +4 -0
- package/dist/helpers/registerGitMobResource.js +4 -0
- package/dist/helpers/registerGitMobResource.test.js +36 -0
- package/dist/helpers/registerGitMobResourceAsTool.js +24 -0
- package/dist/helpers/registerGitMobResourceAsTool.test.js +63 -0
- package/dist/helpers/registerGitMobTool.js +9 -0
- package/dist/helpers/registerGitMobTool.test.js +40 -0
- package/dist/helpers/runCliCommand.js +21 -0
- package/dist/helpers/runCliCommand.test.js +38 -0
- package/dist/index.js +8 -0
- package/dist/index.test.js +22 -0
- package/dist/resources/gitMobHelp.js +29 -0
- package/dist/resources/gitMobHelp.test.js +23 -0
- package/dist/resources/gitMobVersion.js +27 -0
- package/dist/resources/gitMobVersion.test.js +19 -0
- package/dist/resources/index.js +5 -0
- package/dist/resources/mobSessionCoauthorTrailers.js +39 -0
- package/dist/resources/mobSessionCoauthorTrailers.test.js +22 -0
- package/dist/resources/mobSessionCoauthors.js +40 -0
- package/dist/resources/mobSessionCoauthors.test.js +21 -0
- package/dist/resources/teamMembers.js +41 -0
- package/dist/resources/teamMembers.test.js +22 -0
- package/dist/tools/addTeamMember.js +31 -0
- package/dist/tools/addTeamMember.test.js +33 -0
- package/dist/tools/clearMobSession.js +23 -0
- package/dist/tools/clearMobSession.test.js +25 -0
- package/dist/tools/deleteTeamMember.js +28 -0
- package/dist/tools/deleteTeamMember.test.js +30 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/setMobSessionCoauthors.js +29 -0
- package/dist/tools/setMobSessionCoauthors.test.js +31 -0
- package/dist/tools/setupGitMobGlobally.js +27 -0
- package/dist/tools/setupGitMobGlobally.test.js +29 -0
- package/dist/tools/setupGitMobLocally.js +28 -0
- package/dist/tools/setupGitMobLocally.test.js +30 -0
- package/dist/types/GitMobResource.js +1 -0
- package/dist/types/GitMobTool.js +1 -0
- package/package.json +88 -0
package/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Mubashwer Salman Khurshid
|
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,89 @@
|
|
1
|
+
# Git Mob MCP Server
|
2
|
+
|
3
|
+
Node.js server implementing Model Context Protocol (MCP) for [`git mob` CLI app](https://github.com/Mubashwer/git-mob)
|
4
|
+
|
5
|
+
Built using [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk)
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- Setup git mob CLI globally or locally
|
10
|
+
- Add / delete / list team members
|
11
|
+
- Choose team members for pairing / mobbing session
|
12
|
+
- Automatic appending of Co-authored-by in for co-authors in commit messages during pairing / mobbing session
|
13
|
+
|
14
|
+
## API
|
15
|
+
|
16
|
+
### Resources
|
17
|
+
- `git_mob_help`: Displays general help and usage information for the Git Mob CLI.
|
18
|
+
- `git_mob_version`: The installed version of the Git Mob CLI.
|
19
|
+
- `team_members`: List of all the team members that have been added to Git Mob.
|
20
|
+
- `mob_session_coauthors`: List of all coauthors currently included in the active mob or pairing session.
|
21
|
+
- `mob_session_coauthors` (trailers): List of the git Co-authored-by trailers for the coauthors currently included in the active mob or pairing session.
|
22
|
+
|
23
|
+
### Tools
|
24
|
+
- `add_team_member`: Adds a new team member using their key, name, and email.
|
25
|
+
- `clear_mob_session`: Clears the active mob or pairing session.
|
26
|
+
- `delete_team_member`: Deletes a team member by their key.
|
27
|
+
- `set_mob_session_coauthors`: Sets the active pairing or mob session by specifying the keys of the team members to include as coauthors.
|
28
|
+
- `setup_git_mob_globally`: Sets up git-mob globally for the user.
|
29
|
+
- `setup_git_mob_locally`: Sets up git-mob locally for the current repository.
|
30
|
+
|
31
|
+
Because dynamic resources are not yet supported in GitHub Copilot, the resources are also available as tools:
|
32
|
+
- `get_git_mob_help`: Displays general help and usage information for the Git Mob CLI.
|
33
|
+
- `get_git_mob_version`: The installed version of the Git Mob CLI.
|
34
|
+
- `get_team_members`: List of all the team members that have been added to Git Mob.
|
35
|
+
- `get_mob_session_coauthors`: List of all coauthors currently included in the active mob or pairing session.
|
36
|
+
- `get_mob_session_coauthors` (trailers): List of the git Co-authored-by trailers for the coauthors currently included in the active mob or pairing session.
|
37
|
+
|
38
|
+
## Usage with Claude Desktop
|
39
|
+
Add this to your `claude_desktop_config.json`:
|
40
|
+
|
41
|
+
|
42
|
+
### NPX
|
43
|
+
|
44
|
+
```json
|
45
|
+
{
|
46
|
+
"mcpServers": {
|
47
|
+
"gitMob": {
|
48
|
+
"command": "npx",
|
49
|
+
"args": [
|
50
|
+
"-y",
|
51
|
+
"git-mob-mcp-server"
|
52
|
+
]
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
```
|
57
|
+
|
58
|
+
## Usage with VS Code
|
59
|
+
|
60
|
+
For installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open Settings (JSON)`.
|
61
|
+
|
62
|
+
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
63
|
+
|
64
|
+
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
65
|
+
|
66
|
+
You can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.
|
67
|
+
|
68
|
+
|
69
|
+
### NPX
|
70
|
+
|
71
|
+
```json
|
72
|
+
{
|
73
|
+
"mcp": {
|
74
|
+
"servers": {
|
75
|
+
"gitMob": {
|
76
|
+
"command": "npx",
|
77
|
+
"args": [
|
78
|
+
"-y",
|
79
|
+
"git-mob-mcp-server",
|
80
|
+
]
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
```
|
86
|
+
|
87
|
+
## License
|
88
|
+
|
89
|
+
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { runCliCommand } from "../helpers/index.js";
|
2
|
+
const GIT_MOB_CLI = "git-mob";
|
3
|
+
export async function setupGlobal() {
|
4
|
+
return runCliCommand(GIT_MOB_CLI, ["setup", "--global"]);
|
5
|
+
}
|
6
|
+
export async function setupLocal() {
|
7
|
+
return runCliCommand(GIT_MOB_CLI, ["setup", "--local"]);
|
8
|
+
}
|
9
|
+
export async function addCoauthor(key, name, email) {
|
10
|
+
return runCliCommand(GIT_MOB_CLI, ["coauthor", "--add", key, name, email]);
|
11
|
+
}
|
12
|
+
export async function deleteCoauthor(key) {
|
13
|
+
return runCliCommand(GIT_MOB_CLI, ["coauthor", "--delete", key]);
|
14
|
+
}
|
15
|
+
export async function listCoauthors() {
|
16
|
+
return runCliCommand(GIT_MOB_CLI, ["coauthor", "--list"]);
|
17
|
+
}
|
18
|
+
export async function setMobSession(coauthorKeys) {
|
19
|
+
return runCliCommand(GIT_MOB_CLI, ["--with", ...coauthorKeys]);
|
20
|
+
}
|
21
|
+
export async function clearMobSession() {
|
22
|
+
return runCliCommand(GIT_MOB_CLI, ["--clear"]);
|
23
|
+
}
|
24
|
+
export async function listMobSessionCoauthors() {
|
25
|
+
return runCliCommand(GIT_MOB_CLI, ["--list"]);
|
26
|
+
}
|
27
|
+
export async function listMobSessionCoauthorTrailers() {
|
28
|
+
return runCliCommand(GIT_MOB_CLI, ["--trailers"]);
|
29
|
+
}
|
30
|
+
export async function getVersion() {
|
31
|
+
return runCliCommand(GIT_MOB_CLI, ["--version"]);
|
32
|
+
}
|
33
|
+
export async function getHelp(command) {
|
34
|
+
const args = ["help"];
|
35
|
+
if (command)
|
36
|
+
args.push(command);
|
37
|
+
return runCliCommand(GIT_MOB_CLI, args);
|
38
|
+
}
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import { describe, test, expect, beforeEach, jest } from "@jest/globals";
|
2
|
+
import * as gitMobClient from "./gitMobClient";
|
3
|
+
import { runCliCommand } from "../helpers/index.js";
|
4
|
+
jest.mock("../helpers/index.js", () => ({
|
5
|
+
runCliCommand: jest.fn(),
|
6
|
+
}));
|
7
|
+
const mockRunCmd = runCliCommand;
|
8
|
+
describe("[clients] gitMobClient", () => {
|
9
|
+
beforeEach(() => {
|
10
|
+
jest.resetAllMocks();
|
11
|
+
});
|
12
|
+
test("setupGlobal", async () => {
|
13
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: "Setup complete" });
|
14
|
+
const result = await gitMobClient.setupGlobal();
|
15
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", [
|
16
|
+
"setup",
|
17
|
+
"--global",
|
18
|
+
]);
|
19
|
+
expect(result).toEqual({ ok: true, value: "Setup complete" });
|
20
|
+
});
|
21
|
+
test("setupLocal", async () => {
|
22
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: "Setup complete" });
|
23
|
+
const result = await gitMobClient.setupLocal();
|
24
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", ["setup", "--local"]);
|
25
|
+
expect(result).toEqual({ ok: true, value: "Setup complete" });
|
26
|
+
});
|
27
|
+
test("addCoauthor", async () => {
|
28
|
+
mockRunCmd.mockResolvedValue({
|
29
|
+
ok: true,
|
30
|
+
value: "Alice Bob <alice@example.com>",
|
31
|
+
});
|
32
|
+
const result = await gitMobClient.addCoauthor("ab", "Alice Bob", "alice@example.com");
|
33
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", [
|
34
|
+
"coauthor",
|
35
|
+
"--add",
|
36
|
+
"ab",
|
37
|
+
"Alice Bob",
|
38
|
+
"alice@example.com",
|
39
|
+
]);
|
40
|
+
expect(result).toEqual({
|
41
|
+
ok: true,
|
42
|
+
value: "Alice Bob <alice@example.com>",
|
43
|
+
});
|
44
|
+
});
|
45
|
+
test("deleteCoauthor", async () => {
|
46
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: "" });
|
47
|
+
const result = await gitMobClient.deleteCoauthor("ab");
|
48
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", [
|
49
|
+
"coauthor",
|
50
|
+
"--delete",
|
51
|
+
"ab",
|
52
|
+
]);
|
53
|
+
expect(result).toEqual({ ok: true, value: "" });
|
54
|
+
});
|
55
|
+
test("listCoauthors", async () => {
|
56
|
+
const coauthorList = "leo Leo Messi <leo.messi@arg.com>\nab Alice Bob <alice@example.com>";
|
57
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: coauthorList });
|
58
|
+
const result = await gitMobClient.listCoauthors();
|
59
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", [
|
60
|
+
"coauthor",
|
61
|
+
"--list",
|
62
|
+
]);
|
63
|
+
expect(result).toEqual({ ok: true, value: coauthorList });
|
64
|
+
});
|
65
|
+
test("setMobSession", async () => {
|
66
|
+
const session = "leo Leo Messi <leo.messi@arg.com>\nab Alice Bob <alice@example.com>";
|
67
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: session });
|
68
|
+
const result = await gitMobClient.setMobSession(["ab", "cd"]);
|
69
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", [
|
70
|
+
"--with",
|
71
|
+
"ab",
|
72
|
+
"cd",
|
73
|
+
]);
|
74
|
+
expect(result).toEqual({ ok: true, value: session });
|
75
|
+
});
|
76
|
+
test("clearMobSession", async () => {
|
77
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: "" });
|
78
|
+
const result = await gitMobClient.clearMobSession();
|
79
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", ["--clear"]);
|
80
|
+
expect(result).toEqual({ ok: true, value: "" });
|
81
|
+
});
|
82
|
+
test("listMobSessionCoauthors", async () => {
|
83
|
+
const coauthors = "leo Leo Messi <leo.messi@arg.com>\nab Alice Bob <alice@example.com>";
|
84
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: coauthors });
|
85
|
+
const result = await gitMobClient.listMobSessionCoauthors();
|
86
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", ["--list"]);
|
87
|
+
expect(result).toEqual({ ok: true, value: coauthors });
|
88
|
+
});
|
89
|
+
test("listMobSessionCoauthorTrailers", async () => {
|
90
|
+
const trailers = "Co-authored-by: Leo Messi <leo.messi@arg.com>\nCo-authored-by: Alice Bob <alice@example.com>";
|
91
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: trailers });
|
92
|
+
const result = await gitMobClient.listMobSessionCoauthorTrailers();
|
93
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", ["--trailers"]);
|
94
|
+
expect(result).toEqual({ ok: true, value: trailers });
|
95
|
+
});
|
96
|
+
test("getVersion", async () => {
|
97
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: "git-mob-tool 1.6.2" });
|
98
|
+
const result = await gitMobClient.getVersion();
|
99
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", ["--version"]);
|
100
|
+
expect(result).toEqual({ ok: true, value: "git-mob-tool 1.6.2" });
|
101
|
+
});
|
102
|
+
test("getHelp (no command)", async () => {
|
103
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: "help" });
|
104
|
+
const result = await gitMobClient.getHelp();
|
105
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", ["help"]);
|
106
|
+
expect(result).toEqual({ ok: true, value: "help" });
|
107
|
+
});
|
108
|
+
test("getHelp (with command)", async () => {
|
109
|
+
mockRunCmd.mockResolvedValue({ ok: true, value: "help setup" });
|
110
|
+
const result = await gitMobClient.getHelp("setup");
|
111
|
+
expect(runCliCommand).toHaveBeenCalledWith("git-mob", ["help", "setup"]);
|
112
|
+
expect(result).toEqual({ ok: true, value: "help setup" });
|
113
|
+
});
|
114
|
+
});
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
2
|
+
import * as tools from "./tools/index.js";
|
3
|
+
import * as resources from "./resources/index.js";
|
4
|
+
import { registerGitMobTool, registerGitMobResource, registerGtMobResourceAsTool, } from "./helpers/index.js";
|
5
|
+
export const createGitMobServer = () => {
|
6
|
+
const server = new McpServer({
|
7
|
+
name: "Git Mob",
|
8
|
+
version: "1.0.0",
|
9
|
+
}, {
|
10
|
+
capabilities: {
|
11
|
+
tools: {},
|
12
|
+
resources: {},
|
13
|
+
},
|
14
|
+
});
|
15
|
+
registerGitMobResource(server, resources.gitMobVersion);
|
16
|
+
registerGitMobResource(server, resources.gitMobHelp);
|
17
|
+
registerGitMobResource(server, resources.teamMembers);
|
18
|
+
registerGitMobResource(server, resources.mobSessionCoauthors);
|
19
|
+
registerGitMobResource(server, resources.mobSessionCoauthorTrailers);
|
20
|
+
// Currently, Github Copilot does not support dynamic resources in MCP Server,
|
21
|
+
// so we register them as tools as well
|
22
|
+
registerGtMobResourceAsTool(server, resources.gitMobVersion);
|
23
|
+
registerGtMobResourceAsTool(server, resources.gitMobHelp);
|
24
|
+
registerGtMobResourceAsTool(server, resources.teamMembers);
|
25
|
+
registerGtMobResourceAsTool(server, resources.mobSessionCoauthors);
|
26
|
+
registerGtMobResourceAsTool(server, resources.mobSessionCoauthorTrailers);
|
27
|
+
registerGitMobTool(server, tools.setupGitMobGlobally);
|
28
|
+
registerGitMobTool(server, tools.setupGitMobLocally);
|
29
|
+
registerGitMobTool(server, tools.addTeamMember);
|
30
|
+
registerGitMobTool(server, tools.deleteTeamMember);
|
31
|
+
registerGitMobTool(server, tools.setMobSessionCoauthors);
|
32
|
+
registerGitMobTool(server, tools.clearMobSession);
|
33
|
+
return server;
|
34
|
+
};
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import { describe, it, expect, jest } from "@jest/globals";
|
2
|
+
import * as helpers from "./helpers/index.js";
|
3
|
+
import * as resources from "./resources/index.js";
|
4
|
+
import * as tools from "./tools/index.js";
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
6
|
+
import { createGitMobServer } from "./gitMobServerFactory.js";
|
7
|
+
jest.mock("./helpers/index.js", () => ({
|
8
|
+
registerGitMobTool: jest.fn(),
|
9
|
+
registerGitMobResource: jest.fn(),
|
10
|
+
registerGtMobResourceAsTool: jest.fn(),
|
11
|
+
}));
|
12
|
+
jest.mock("@modelcontextprotocol/sdk/server/mcp.js", () => {
|
13
|
+
const actual = jest.requireActual("@modelcontextprotocol/sdk/server/mcp.js");
|
14
|
+
return {
|
15
|
+
...actual,
|
16
|
+
McpServer: jest.fn().mockImplementation((meta, opts) => ({ meta, opts })),
|
17
|
+
};
|
18
|
+
});
|
19
|
+
describe("gitMobServerFactory: createGitMobServer", () => {
|
20
|
+
const mockMCPServer = McpServer;
|
21
|
+
it("should create an instance of McpServer for Git Mob", () => {
|
22
|
+
createGitMobServer();
|
23
|
+
expect(mockMCPServer).toHaveBeenCalledWith({ name: "Git Mob", version: "1.0.0" }, {
|
24
|
+
capabilities: {
|
25
|
+
tools: {},
|
26
|
+
resources: {},
|
27
|
+
},
|
28
|
+
});
|
29
|
+
});
|
30
|
+
it("should register all resources", async () => {
|
31
|
+
const { registerGitMobResource } = helpers;
|
32
|
+
const server = createGitMobServer();
|
33
|
+
expect(registerGitMobResource).toHaveBeenCalledWith(server, resources.gitMobVersion);
|
34
|
+
expect(registerGitMobResource).toHaveBeenCalledWith(server, resources.gitMobHelp);
|
35
|
+
expect(registerGitMobResource).toHaveBeenCalledWith(server, resources.teamMembers);
|
36
|
+
expect(registerGitMobResource).toHaveBeenCalledWith(server, resources.mobSessionCoauthors);
|
37
|
+
expect(registerGitMobResource).toHaveBeenCalledWith(server, resources.mobSessionCoauthorTrailers);
|
38
|
+
});
|
39
|
+
it("should register all resources as tools", async () => {
|
40
|
+
const { registerGtMobResourceAsTool } = helpers;
|
41
|
+
const server = createGitMobServer();
|
42
|
+
expect(registerGtMobResourceAsTool).toHaveBeenCalledWith(server, resources.gitMobVersion);
|
43
|
+
expect(registerGtMobResourceAsTool).toHaveBeenCalledWith(server, resources.gitMobHelp);
|
44
|
+
expect(registerGtMobResourceAsTool).toHaveBeenCalledWith(server, resources.teamMembers);
|
45
|
+
expect(registerGtMobResourceAsTool).toHaveBeenCalledWith(server, resources.mobSessionCoauthors);
|
46
|
+
expect(registerGtMobResourceAsTool).toHaveBeenCalledWith(server, resources.mobSessionCoauthorTrailers);
|
47
|
+
});
|
48
|
+
it("should register all tools", async () => {
|
49
|
+
const { registerGitMobTool } = helpers;
|
50
|
+
const server = createGitMobServer();
|
51
|
+
expect(registerGitMobTool).toHaveBeenCalledWith(server, tools.setupGitMobGlobally);
|
52
|
+
expect(registerGitMobTool).toHaveBeenCalledWith(server, tools.setupGitMobLocally);
|
53
|
+
expect(registerGitMobTool).toHaveBeenCalledWith(server, tools.addTeamMember);
|
54
|
+
expect(registerGitMobTool).toHaveBeenCalledWith(server, tools.deleteTeamMember);
|
55
|
+
expect(registerGitMobTool).toHaveBeenCalledWith(server, tools.setMobSessionCoauthors);
|
56
|
+
expect(registerGitMobTool).toHaveBeenCalledWith(server, tools.clearMobSession);
|
57
|
+
});
|
58
|
+
});
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { describe, it, expect, jest } from "@jest/globals";
|
2
|
+
import { registerGitMobResource } from "./registerGitMobResource";
|
3
|
+
import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
4
|
+
describe("[helpers] registerGitMobResource", () => {
|
5
|
+
it("should register given git mob resource with the given server", () => {
|
6
|
+
const mockServer = {
|
7
|
+
resource: jest.fn(),
|
8
|
+
};
|
9
|
+
const name = "test_resource";
|
10
|
+
const template = new ResourceTemplate("gitmob://test-resource", {
|
11
|
+
list: undefined,
|
12
|
+
});
|
13
|
+
const metadata = {
|
14
|
+
description: "This is a test resource for testing purposes.",
|
15
|
+
mimeType: "text/plain",
|
16
|
+
};
|
17
|
+
const readCallback = async (uri) => {
|
18
|
+
return {
|
19
|
+
contents: [
|
20
|
+
{
|
21
|
+
uri: uri.href,
|
22
|
+
text: "hello world",
|
23
|
+
},
|
24
|
+
],
|
25
|
+
};
|
26
|
+
};
|
27
|
+
const testResource = {
|
28
|
+
name,
|
29
|
+
template,
|
30
|
+
metadata,
|
31
|
+
readCallback,
|
32
|
+
};
|
33
|
+
registerGitMobResource(mockServer, testResource);
|
34
|
+
expect(mockServer.resource).toHaveBeenCalledWith(testResource.name, testResource.template, testResource.metadata, testResource.readCallback);
|
35
|
+
});
|
36
|
+
});
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { URL } from "node:url";
|
2
|
+
export const registerGtMobResourceAsTool = (server, resource) => {
|
3
|
+
const { name, template, metadata, readCallback } = resource;
|
4
|
+
const toolName = `get_${name}`;
|
5
|
+
const annotations = {
|
6
|
+
title: toolName,
|
7
|
+
readOnlyHint: true,
|
8
|
+
destructiveHint: false,
|
9
|
+
idempotentHint: true,
|
10
|
+
openWorldHint: false,
|
11
|
+
};
|
12
|
+
const toolCallback = async (args, extra) => {
|
13
|
+
const result = await readCallback(new URL(template.uriTemplate), args, extra);
|
14
|
+
const text = result.contents
|
15
|
+
.map((content) => (typeof content.text === "string" ? content.text : ""))
|
16
|
+
.join("\n");
|
17
|
+
return { content: [{ type: "text", text }] };
|
18
|
+
};
|
19
|
+
server.registerTool(toolName, {
|
20
|
+
description: String(metadata.description),
|
21
|
+
inputSchema: {},
|
22
|
+
annotations,
|
23
|
+
}, toolCallback);
|
24
|
+
};
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { describe, it, expect, jest } from "@jest/globals";
|
2
|
+
import { registerGtMobResourceAsTool } from "./registerGitMobResourceAsTool";
|
3
|
+
import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
4
|
+
describe("[helpers] registerGtMobResourceAsTool", () => {
|
5
|
+
const RESOURCE_CALLBACK_CONTENT = "hello world";
|
6
|
+
const arrangeMockServerAndTestResource = () => {
|
7
|
+
const mockServer = {
|
8
|
+
registerTool: jest.fn(),
|
9
|
+
};
|
10
|
+
const name = "test_resource";
|
11
|
+
const template = new ResourceTemplate("gitmob://test-resource", {
|
12
|
+
list: undefined,
|
13
|
+
});
|
14
|
+
const metadata = {
|
15
|
+
description: "This is a test resource for testing purposes.",
|
16
|
+
mimeType: "text/plain",
|
17
|
+
};
|
18
|
+
const readCallback = async (uri) => {
|
19
|
+
return {
|
20
|
+
contents: [
|
21
|
+
{
|
22
|
+
uri: uri.href,
|
23
|
+
text: RESOURCE_CALLBACK_CONTENT,
|
24
|
+
},
|
25
|
+
],
|
26
|
+
};
|
27
|
+
};
|
28
|
+
const testResource = {
|
29
|
+
name,
|
30
|
+
template,
|
31
|
+
metadata,
|
32
|
+
readCallback,
|
33
|
+
};
|
34
|
+
return { mockServer, testResource };
|
35
|
+
};
|
36
|
+
it("should register a resource as a tool with the given server", async () => {
|
37
|
+
const { mockServer, testResource } = arrangeMockServerAndTestResource();
|
38
|
+
registerGtMobResourceAsTool(mockServer, testResource);
|
39
|
+
expect(mockServer.registerTool).toHaveBeenCalledWith(`get_${testResource.name}`, {
|
40
|
+
description: String(testResource.metadata.description),
|
41
|
+
inputSchema: {},
|
42
|
+
annotations: {
|
43
|
+
title: `get_${testResource.name}`,
|
44
|
+
readOnlyHint: true,
|
45
|
+
destructiveHint: false,
|
46
|
+
idempotentHint: true,
|
47
|
+
openWorldHint: false,
|
48
|
+
},
|
49
|
+
}, expect.any(Function));
|
50
|
+
});
|
51
|
+
it("should have registered tool callback return the resource callback content when invoked", async () => {
|
52
|
+
const { mockServer, testResource } = arrangeMockServerAndTestResource();
|
53
|
+
const readCallBackSpy = jest.spyOn(testResource, "readCallback");
|
54
|
+
registerGtMobResourceAsTool(mockServer, testResource);
|
55
|
+
const toolCallback = mockServer.registerTool.mock
|
56
|
+
.calls[0][2];
|
57
|
+
const result = await toolCallback({}, {});
|
58
|
+
expect(readCallBackSpy).toHaveBeenCalled();
|
59
|
+
expect(result).toEqual({
|
60
|
+
content: [{ type: "text", text: RESOURCE_CALLBACK_CONTENT }],
|
61
|
+
});
|
62
|
+
});
|
63
|
+
});
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import { describe, it, expect, jest } from "@jest/globals";
|
2
|
+
import { registerGitMobTool } from "./registerGitMobTool";
|
3
|
+
import { z } from "zod";
|
4
|
+
describe("[helpers] registerGitMobTool", () => {
|
5
|
+
it("should register given git mob tool with the given server", () => {
|
6
|
+
const mockServer = {
|
7
|
+
registerTool: jest.fn(),
|
8
|
+
};
|
9
|
+
const name = "test_tool";
|
10
|
+
const description = "This is a tool for testing purposes.";
|
11
|
+
const inputSchema = {
|
12
|
+
foo: z.string(),
|
13
|
+
bar: z.string(),
|
14
|
+
};
|
15
|
+
const annotations = {
|
16
|
+
title: "Test me",
|
17
|
+
readOnlyHint: false,
|
18
|
+
destructiveHint: false,
|
19
|
+
idempotentHint: false,
|
20
|
+
openWorldHint: false,
|
21
|
+
};
|
22
|
+
const callback = async ({ foo, bar }) => {
|
23
|
+
return { content: [{ type: "text", text: foo + bar }] };
|
24
|
+
};
|
25
|
+
const testTool = {
|
26
|
+
name,
|
27
|
+
description,
|
28
|
+
inputSchema,
|
29
|
+
annotations,
|
30
|
+
callback,
|
31
|
+
};
|
32
|
+
registerGitMobTool(mockServer, testTool);
|
33
|
+
expect(mockServer.registerTool).toHaveBeenCalledWith(testTool.name, {
|
34
|
+
description: testTool.description,
|
35
|
+
inputSchema: testTool.inputSchema,
|
36
|
+
outputSchema: testTool.outputSchema,
|
37
|
+
annotations: testTool.annotations,
|
38
|
+
}, testTool.callback);
|
39
|
+
});
|
40
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { execFile } from "child_process";
|
2
|
+
import { promisify } from "util";
|
3
|
+
const execFileAsync = promisify(execFile);
|
4
|
+
export async function runCliCommand(program, args) {
|
5
|
+
try {
|
6
|
+
const { stdout, stderr } = await execFileAsync(program, args);
|
7
|
+
return { ok: true, value: stdout || stderr || "" };
|
8
|
+
}
|
9
|
+
catch (error) {
|
10
|
+
if (typeof error === "object" && error !== null) {
|
11
|
+
const { stdout, stderr, message } = error;
|
12
|
+
if (stderr)
|
13
|
+
return { ok: false, value: stderr };
|
14
|
+
if (stdout)
|
15
|
+
return { ok: false, value: stdout };
|
16
|
+
if (message)
|
17
|
+
return { ok: false, value: message };
|
18
|
+
}
|
19
|
+
return { ok: false, value: String(error) };
|
20
|
+
}
|
21
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
2
|
+
import { runCliCommand } from "./runCliCommand";
|
3
|
+
describe("[helpers] runCliCommand", () => {
|
4
|
+
it("should return ok and stdout for a successful command", async () => {
|
5
|
+
const result = await runCliCommand("node", ["-e", "console.log('hello')"]);
|
6
|
+
expect(result.ok).toBe(true);
|
7
|
+
expect(result.value.trim()).toBe("hello");
|
8
|
+
});
|
9
|
+
it("should return ok and stderr for a command that writes to stderr but exits 0", async () => {
|
10
|
+
const result = await runCliCommand("node", [
|
11
|
+
"-e",
|
12
|
+
"console.error('err'); process.exit(0)",
|
13
|
+
]);
|
14
|
+
expect(result.ok).toBe(true);
|
15
|
+
expect(result.value.trim()).toBe("err");
|
16
|
+
});
|
17
|
+
it("should return ok: false and stderr for a command that fails with stderr", async () => {
|
18
|
+
const result = await runCliCommand("node", [
|
19
|
+
"-e",
|
20
|
+
"console.error('fail'); process.exit(1)",
|
21
|
+
]);
|
22
|
+
expect(result.ok).toBe(false);
|
23
|
+
expect(result.value.trim()).toBe("fail");
|
24
|
+
});
|
25
|
+
it("should return ok: false and stdout for a command that fails with stdout only", async () => {
|
26
|
+
const result = await runCliCommand("node", [
|
27
|
+
"-e",
|
28
|
+
"console.log('failout'); process.exit(1)",
|
29
|
+
]);
|
30
|
+
expect(result.ok).toBe(false);
|
31
|
+
expect(result.value.trim()).toBe("failout");
|
32
|
+
});
|
33
|
+
it("should return ok: false and message for a non-existent command", async () => {
|
34
|
+
const result = await runCliCommand("nonexistent-cmd-xyz", []);
|
35
|
+
expect(result.ok).toBe(false);
|
36
|
+
expect(result.value.trim()).toBe("spawn nonexistent-cmd-xyz ENOENT");
|
37
|
+
});
|
38
|
+
});
|
package/dist/index.js
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
2
|
+
import { createGitMobServer } from "./gitMobServerFactory.js";
|
3
|
+
(async () => {
|
4
|
+
const server = createGitMobServer();
|
5
|
+
// Start receiving messages on stdin and sending messages on stdout
|
6
|
+
const transport = new StdioServerTransport();
|
7
|
+
await server.connect(transport);
|
8
|
+
})();
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
2
|
+
import { createGitMobServer } from "./gitMobServerFactory.js";
|
3
|
+
// Mock StdioServerTransport from the MCP SDK
|
4
|
+
const transport = {};
|
5
|
+
const mockConnect = jest.fn();
|
6
|
+
const mockStdioServerTransport = jest.fn().mockImplementation(() => transport);
|
7
|
+
jest.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({
|
8
|
+
StdioServerTransport: mockStdioServerTransport,
|
9
|
+
}));
|
10
|
+
// Mock createGitMobServer to return a mock server with a connect method
|
11
|
+
jest.mock("./gitMobServerFactory.js", () => ({
|
12
|
+
createGitMobServer: jest.fn(() => ({ connect: mockConnect })),
|
13
|
+
}));
|
14
|
+
describe("index.ts", () => {
|
15
|
+
it("should create a server, transport, and connect them", async () => {
|
16
|
+
// Dynamically import index.ts to run its top-level code
|
17
|
+
await import("./index.js");
|
18
|
+
expect(createGitMobServer).toHaveBeenCalled();
|
19
|
+
expect(mockStdioServerTransport).toHaveBeenCalled();
|
20
|
+
expect(mockConnect).toHaveBeenCalledWith(transport);
|
21
|
+
});
|
22
|
+
});
|