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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/dist/clients/gitMobClient.js +38 -0
  4. package/dist/clients/gitMobClient.test.js +114 -0
  5. package/dist/gitMobServerFactory.js +34 -0
  6. package/dist/gitMobServerFactory.test.js +58 -0
  7. package/dist/helpers/index.js +4 -0
  8. package/dist/helpers/registerGitMobResource.js +4 -0
  9. package/dist/helpers/registerGitMobResource.test.js +36 -0
  10. package/dist/helpers/registerGitMobResourceAsTool.js +24 -0
  11. package/dist/helpers/registerGitMobResourceAsTool.test.js +63 -0
  12. package/dist/helpers/registerGitMobTool.js +9 -0
  13. package/dist/helpers/registerGitMobTool.test.js +40 -0
  14. package/dist/helpers/runCliCommand.js +21 -0
  15. package/dist/helpers/runCliCommand.test.js +38 -0
  16. package/dist/index.js +8 -0
  17. package/dist/index.test.js +22 -0
  18. package/dist/resources/gitMobHelp.js +29 -0
  19. package/dist/resources/gitMobHelp.test.js +23 -0
  20. package/dist/resources/gitMobVersion.js +27 -0
  21. package/dist/resources/gitMobVersion.test.js +19 -0
  22. package/dist/resources/index.js +5 -0
  23. package/dist/resources/mobSessionCoauthorTrailers.js +39 -0
  24. package/dist/resources/mobSessionCoauthorTrailers.test.js +22 -0
  25. package/dist/resources/mobSessionCoauthors.js +40 -0
  26. package/dist/resources/mobSessionCoauthors.test.js +21 -0
  27. package/dist/resources/teamMembers.js +41 -0
  28. package/dist/resources/teamMembers.test.js +22 -0
  29. package/dist/tools/addTeamMember.js +31 -0
  30. package/dist/tools/addTeamMember.test.js +33 -0
  31. package/dist/tools/clearMobSession.js +23 -0
  32. package/dist/tools/clearMobSession.test.js +25 -0
  33. package/dist/tools/deleteTeamMember.js +28 -0
  34. package/dist/tools/deleteTeamMember.test.js +30 -0
  35. package/dist/tools/index.js +6 -0
  36. package/dist/tools/setMobSessionCoauthors.js +29 -0
  37. package/dist/tools/setMobSessionCoauthors.test.js +31 -0
  38. package/dist/tools/setupGitMobGlobally.js +27 -0
  39. package/dist/tools/setupGitMobGlobally.test.js +29 -0
  40. package/dist/tools/setupGitMobLocally.js +28 -0
  41. package/dist/tools/setupGitMobLocally.test.js +30 -0
  42. package/dist/types/GitMobResource.js +1 -0
  43. package/dist/types/GitMobTool.js +1 -0
  44. 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,4 @@
1
+ export * from "./registerGitMobTool.js";
2
+ export * from "./registerGitMobResource.js";
3
+ export * from "./registerGitMobResourceAsTool.js";
4
+ export * from "./runCliCommand.js";
@@ -0,0 +1,4 @@
1
+ export const registerGitMobResource = (server, resource) => {
2
+ const { name, template, metadata, readCallback } = resource;
3
+ server.resource(name, template, metadata, readCallback);
4
+ };
@@ -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,9 @@
1
+ export const registerGitMobTool = (server, tool) => {
2
+ const { name, description, inputSchema, outputSchema, annotations, callback, } = tool;
3
+ server.registerTool(name, {
4
+ description,
5
+ inputSchema,
6
+ outputSchema,
7
+ annotations,
8
+ }, callback);
9
+ };
@@ -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
+ });