@supernova123/docker-mcp-server 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.
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Mock Dockerode before importing the module under test
4
+ const mockListContainers = vi.fn();
5
+ const mockInspect = vi.fn();
6
+ const mockStart = vi.fn();
7
+ const mockStop = vi.fn();
8
+ const mockRestart = vi.fn();
9
+ const mockRemove = vi.fn();
10
+ const mockCreateContainer = vi.fn();
11
+
12
+ vi.mock("dockerode", () => {
13
+ return {
14
+ default: vi.fn().mockImplementation(() => ({
15
+ listContainers: mockListContainers,
16
+ getContainer: vi.fn().mockReturnValue({
17
+ inspect: mockInspect,
18
+ start: mockStart,
19
+ stop: mockStop,
20
+ restart: mockRestart,
21
+ remove: mockRemove,
22
+ }),
23
+ createContainer: mockCreateContainer,
24
+ })),
25
+ };
26
+ });
27
+
28
+ import { registerContainerTools } from "../src/tools/container.js";
29
+ import { createDockerClient } from "../src/docker.js";
30
+
31
+ // Minimal MCP server mock
32
+ function createMockServer() {
33
+ const tools: Record<string, { description: string; handler: Function }> = {};
34
+ return {
35
+ tool: (name: string, description: string, _schema: unknown, handler: Function) => {
36
+ tools[name] = { description, handler };
37
+ },
38
+ tools,
39
+ };
40
+ }
41
+
42
+ describe("Container Tools", () => {
43
+ let server: ReturnType<typeof createMockServer>;
44
+ let docker: ReturnType<typeof createDockerClient>;
45
+
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ server = createMockServer();
49
+ docker = createDockerClient();
50
+ registerContainerTools(server as any, docker);
51
+ });
52
+
53
+ describe("list_containers", () => {
54
+ it("returns formatted container list", async () => {
55
+ mockListContainers.mockResolvedValue([
56
+ {
57
+ Id: "abc123def456",
58
+ Names: ["/my-container"],
59
+ Image: "nginx:latest",
60
+ State: "running",
61
+ Status: "Up 2 hours",
62
+ Created: Date.now() / 1000,
63
+ Ports: [{ PrivatePort: 80, PublicPort: 8080, Type: "tcp" }],
64
+ Labels: { "com.example.env": "prod" },
65
+ Mounts: [],
66
+ },
67
+ ]);
68
+
69
+ const result = await server.tools["list_containers"].handler({ all: true });
70
+ const data = JSON.parse(result.content[0].text);
71
+
72
+ expect(data).toHaveLength(1);
73
+ expect(data[0].id).toBe("abc123def456");
74
+ expect(data[0].name).toBe("my-container");
75
+ expect(data[0].image).toBe("nginx:latest");
76
+ expect(data[0].state).toBe("running");
77
+ });
78
+
79
+ it("calls listContainers with correct filters", async () => {
80
+ mockListContainers.mockResolvedValue([]);
81
+ await server.tools["list_containers"].handler({ state: "running", name: "web" });
82
+
83
+ const callArgs = mockListContainers.mock.calls[0][0];
84
+ expect(callArgs.all).toBe(false);
85
+ const filters = JSON.parse(callArgs.filters);
86
+ expect(filters.status).toEqual(["running"]);
87
+ expect(filters.name).toEqual(["/web"]);
88
+ });
89
+ });
90
+
91
+ describe("inspect_container", () => {
92
+ it("returns full container inspection", async () => {
93
+ mockInspect.mockResolvedValue({
94
+ Id: "abc123",
95
+ Config: { Image: "nginx", Env: ["FOO=bar"] },
96
+ State: { Running: true },
97
+ });
98
+
99
+ const result = await server.tools["inspect_container"].handler({ container_id: "abc123" });
100
+ const data = JSON.parse(result.content[0].text);
101
+
102
+ expect(data.Id).toBe("abc123");
103
+ expect(data.Config.Image).toBe("nginx");
104
+ });
105
+ });
106
+
107
+ describe("start_container", () => {
108
+ it("starts a container and returns success message", async () => {
109
+ mockStart.mockResolvedValue(undefined);
110
+ const result = await server.tools["start_container"].handler({ container_id: "abc123" });
111
+
112
+ expect(result.content[0].text).toContain("started");
113
+ expect(mockStart).toHaveBeenCalled();
114
+ });
115
+ });
116
+
117
+ describe("stop_container", () => {
118
+ it("stops with default timeout", async () => {
119
+ mockStop.mockResolvedValue(undefined);
120
+ await server.tools["stop_container"].handler({ container_id: "abc123" });
121
+
122
+ expect(mockStop).toHaveBeenCalledWith({ t: 10 });
123
+ });
124
+
125
+ it("stops with custom timeout", async () => {
126
+ mockStop.mockResolvedValue(undefined);
127
+ await server.tools["stop_container"].handler({ container_id: "abc123", timeout: 30 });
128
+
129
+ expect(mockStop).toHaveBeenCalledWith({ t: 30 });
130
+ });
131
+ });
132
+
133
+ describe("restart_container", () => {
134
+ it("restarts with default timeout", async () => {
135
+ mockRestart.mockResolvedValue(undefined);
136
+ await server.tools["restart_container"].handler({ container_id: "abc123" });
137
+
138
+ expect(mockRestart).toHaveBeenCalledWith({ t: 10 });
139
+ });
140
+ });
141
+
142
+ describe("remove_container", () => {
143
+ it("removes without force by default", async () => {
144
+ mockRemove.mockResolvedValue(undefined);
145
+ await server.tools["remove_container"].handler({ container_id: "abc123" });
146
+
147
+ expect(mockRemove).toHaveBeenCalledWith({ force: false });
148
+ });
149
+
150
+ it("removes with force when requested", async () => {
151
+ mockRemove.mockResolvedValue(undefined);
152
+ await server.tools["remove_container"].handler({ container_id: "abc123", force: true });
153
+
154
+ expect(mockRemove).toHaveBeenCalledWith({ force: true });
155
+ });
156
+ });
157
+
158
+ describe("run_container", () => {
159
+ it("creates and starts a container with full options", async () => {
160
+ mockCreateContainer.mockResolvedValue({ id: "new123abc456", start: mockStart });
161
+ mockStart.mockResolvedValue(undefined);
162
+
163
+ const result = await server.tools["run_container"].handler({
164
+ image: "nginx:latest",
165
+ name: "web",
166
+ env: { PORT: "8080" },
167
+ ports: { "80/tcp": "8080" },
168
+ restart_policy: "unless-stopped",
169
+ });
170
+
171
+ expect(result.content[0].text).toContain("new123abc456");
172
+ expect(mockCreateContainer).toHaveBeenCalledWith(
173
+ expect.objectContaining({
174
+ Image: "nginx:latest",
175
+ name: "web",
176
+ Env: ["PORT=8080"],
177
+ })
178
+ );
179
+ expect(mockStart).toHaveBeenCalled();
180
+ });
181
+ });
182
+
183
+ describe("error handling", () => {
184
+ it("returns error on failure", async () => {
185
+ mockListContainers.mockRejectedValue(new Error("Docker daemon not running"));
186
+ const result = await server.tools["list_containers"].handler({});
187
+
188
+ expect(result.isError).toBe(true);
189
+ expect(result.content[0].text).toContain("Docker daemon not running");
190
+ });
191
+ });
192
+ });
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockListImages = vi.fn();
4
+ const mockPull = vi.fn();
5
+ const mockBuildImage = vi.fn();
6
+ const mockImageRemove = vi.fn();
7
+ const mockFollowProgress = vi.fn();
8
+
9
+ vi.mock("dockerode", () => {
10
+ return {
11
+ default: vi.fn().mockImplementation(() => ({
12
+ listImages: mockListImages,
13
+ pull: mockPull,
14
+ buildImage: mockBuildImage,
15
+ getImage: vi.fn().mockReturnValue({
16
+ remove: mockImageRemove,
17
+ }),
18
+ modem: {
19
+ followProgress: mockFollowProgress,
20
+ },
21
+ })),
22
+ };
23
+ });
24
+
25
+ import { registerImageTools } from "../src/tools/image.js";
26
+ import { createDockerClient } from "../src/docker.js";
27
+
28
+ function createMockServer() {
29
+ const tools: Record<string, { description: string; handler: Function }> = {};
30
+ return {
31
+ tool: (name: string, description: string, _schema: unknown, handler: Function) => {
32
+ tools[name] = { description, handler };
33
+ },
34
+ tools,
35
+ };
36
+ }
37
+
38
+ describe("Image Tools", () => {
39
+ let server: ReturnType<typeof createMockServer>;
40
+ let docker: ReturnType<typeof createDockerClient>;
41
+
42
+ beforeEach(() => {
43
+ vi.clearAllMocks();
44
+ server = createMockServer();
45
+ docker = createDockerClient();
46
+ registerImageTools(server as any, docker);
47
+ });
48
+
49
+ describe("list_images", () => {
50
+ it("returns formatted image list", async () => {
51
+ mockListImages.mockResolvedValue([
52
+ {
53
+ Id: "sha256:abc123def456789",
54
+ RepoTags: ["nginx:latest", "nginx:1.25"],
55
+ Size: 187000000,
56
+ Created: "2024-01-15T10:00:00Z",
57
+ },
58
+ ]);
59
+
60
+ const result = await server.tools["list_images"].handler({});
61
+ const data = JSON.parse(result.content[0].text);
62
+
63
+ expect(data).toHaveLength(1);
64
+ expect(data[0].tags).toEqual(["nginx:latest", "nginx:1.25"]);
65
+ expect(data[0].size).toBe(187000000);
66
+ });
67
+
68
+ it("handles images with no tags", async () => {
69
+ mockListImages.mockResolvedValue([
70
+ {
71
+ Id: "sha256:abc123",
72
+ RepoTags: null,
73
+ Size: 1000,
74
+ Created: "2024-01-15T10:00:00Z",
75
+ },
76
+ ]);
77
+
78
+ const result = await server.tools["list_images"].handler({});
79
+ const data = JSON.parse(result.content[0].text);
80
+
81
+ expect(data[0].tags).toEqual(["<none>:<none>"]);
82
+ });
83
+
84
+ it("applies filter when specified", async () => {
85
+ mockListImages.mockResolvedValue([]);
86
+ await server.tools["list_images"].handler({ filter: "nginx" });
87
+
88
+ expect(mockListImages).toHaveBeenCalledWith({
89
+ all: false,
90
+ filters: JSON.stringify({ reference: ["nginx"] }),
91
+ });
92
+ });
93
+ });
94
+
95
+ describe("pull_image", () => {
96
+ it("pulls an image with tag", async () => {
97
+ const mockStream = {};
98
+ mockPull.mockResolvedValue(mockStream);
99
+ mockFollowProgress.mockImplementation((_stream: unknown, cb: Function) => {
100
+ cb(null);
101
+ });
102
+
103
+ const result = await server.tools["pull_image"].handler({
104
+ image: "nginx",
105
+ tag: "latest",
106
+ });
107
+
108
+ expect(result.content[0].text).toContain("successfully");
109
+ expect(mockPull).toHaveBeenCalledWith("nginx:latest");
110
+ });
111
+
112
+ it("pulls an image without tag", async () => {
113
+ mockPull.mockResolvedValue({});
114
+ mockFollowProgress.mockImplementation((_stream: unknown, cb: Function) => {
115
+ cb(null);
116
+ });
117
+
118
+ await server.tools["pull_image"].handler({ image: "alpine" });
119
+
120
+ expect(mockPull).toHaveBeenCalledWith("alpine");
121
+ });
122
+ });
123
+
124
+ describe("remove_image", () => {
125
+ it("removes image without force", async () => {
126
+ mockImageRemove.mockResolvedValue(undefined);
127
+
128
+ const result = await server.tools["remove_image"].handler({ image: "nginx:old" });
129
+
130
+ expect(result.content[0].text).toContain("removed");
131
+ expect(mockImageRemove).toHaveBeenCalledWith({ force: false });
132
+ });
133
+
134
+ it("removes image with force", async () => {
135
+ mockImageRemove.mockResolvedValue(undefined);
136
+
137
+ await server.tools["remove_image"].handler({ image: "nginx:old", force: true });
138
+
139
+ expect(mockImageRemove).toHaveBeenCalledWith({ force: true });
140
+ });
141
+ });
142
+
143
+ describe("error handling", () => {
144
+ it("returns error on list failure", async () => {
145
+ mockListImages.mockRejectedValue(new Error("Cannot connect to Docker"));
146
+ const result = await server.tools["list_images"].handler({});
147
+
148
+ expect(result.isError).toBe(true);
149
+ expect(result.content[0].text).toContain("Cannot connect to Docker");
150
+ });
151
+
152
+ it("returns error on pull failure", async () => {
153
+ mockPull.mockRejectedValue(new Error("image not found"));
154
+ const result = await server.tools["pull_image"].handler({ image: "nonexistent" });
155
+
156
+ expect(result.isError).toBe(true);
157
+ expect(result.content[0].text).toContain("image not found");
158
+ });
159
+ });
160
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "tests"]
19
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ },
8
+ });