Skip to content

Commit 46a66f5

Browse files
authored
Fix issue with @latest for npx workloads (#1694)
The change introduced in #1601 introduced a bug when a user specified @latest as the version of the npx package. The build process would create a container with the latest version of the MCP server, but npx would fail because npx will always check npm to see what the latest version is, and the --no-install flag blocked it from doing so. Do some tweaking to drop the version specifier when npx is run. This will cause npx to just use the locally installed module that was populated during the build.
1 parent 4e9e38f commit 46a66f5

File tree

3 files changed

+104
-6
lines changed

3 files changed

+104
-6
lines changed

pkg/container/templates/npx.tmpl

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ COPY --from=builder --chown=appuser:appgroup /build/ /app/
8585
{{else}}
8686
# Copy package.json to maintain the dependency tree
8787
COPY --from=builder --chown=appuser:appgroup /build/package.json /app/package.json
88+
COPY --from=builder --chown=appuser:appgroup /build/package-lock.json /app/package-lock.json
8889
{{end}}
8990

9091
# Set NODE_PATH to find the pre-installed modules
@@ -94,6 +95,11 @@ ENV NODE_PATH=/app/node_modules \
9495
# Switch to non-root user
9596
USER appuser
9697

97-
# Run the pre-installed MCP package directly using npx
98-
# The package is already installed, so npx will use the local version without downloading
99-
ENTRYPOINT ["npx", "--no-install", "{{.MCPPackage}}"{{range .MCPArgs}}, "{{.}}"{{end}}]
98+
# `MCPPackage` may include a version suffix (e.g., `[email protected]`), which we cannot use here.
99+
# Create a small wrapper script to handle this.
100+
RUN echo "#!/bin/sh" >> entrypoint.sh && \
101+
echo "exec npx $(echo {{.MCPPackage}} | sed 's/@[^@/]*$//'){{range .MCPArgs}}, "{{.}}"{{end}}" >> entrypoint.sh && \
102+
chmod +x entrypoint.sh
103+
104+
# Run the preinstalled MCP package directly using npx.
105+
ENTRYPOINT ["./entrypoint.sh"]

pkg/container/templates/templates_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ func TestGetDockerfileTemplate(t *testing.T) {
8282
"FROM node:",
8383
"npm install --save example-package",
8484
"COPY --from=builder --chown=appuser:appgroup /build/node_modules /app/node_modules",
85-
"ENTRYPOINT [\"npx\", \"--no-install\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]",
85+
"echo \"exec npx $(echo example-package | sed 's/@[^@/]*$//'), \"--arg1\", \"--arg2\", \"value\"\" >> entrypoint.sh",
86+
"ENTRYPOINT [\"./entrypoint.sh\"]",
8687
},
8788
wantMatches: []string{
8889
`FROM node:\d+-alpine AS builder`, // Match builder stage
@@ -105,7 +106,8 @@ func TestGetDockerfileTemplate(t *testing.T) {
105106
wantContains: []string{
106107
"FROM node:",
107108
"npm install --save example-package",
108-
"ENTRYPOINT [\"npx\", \"--no-install\", \"example-package\", \"--arg1\", \"--arg2\", \"value\"]",
109+
"echo \"exec npx $(echo example-package | sed 's/@[^@/]*$//'), \"--arg1\", \"--arg2\", \"value\"\" >> entrypoint.sh",
110+
"ENTRYPOINT [\"./entrypoint.sh\"]",
109111
"Add custom CA certificate BEFORE any network operations",
110112
"COPY ca-cert.crt /tmp/custom-ca.crt",
111113
"cat /tmp/custom-ca.crt >> /etc/ssl/certs/ca-certificates.crt",

test/e2e/protocol_builds_e2e_test.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var _ = Describe("Protocol Builds E2E", Serial, func() {
3232
var serverName string
3333

3434
BeforeEach(func() {
35-
serverName = generateUniqueProtocolServerName("sequential-thinking-test")
35+
serverName = generateUniqueProtocolServerName("sequential-thinking-noversion-test")
3636
})
3737

3838
AfterEach(func() {
@@ -73,6 +73,96 @@ var _ = Describe("Protocol Builds E2E", Serial, func() {
7373
})
7474
})
7575

76+
Context("when starting @modelcontextprotocol/server-sequential-thinking@latest", func() {
77+
var serverName string
78+
79+
BeforeEach(func() {
80+
serverName = generateUniqueProtocolServerName("sequential-thinking-latest-test")
81+
})
82+
83+
AfterEach(func() {
84+
if config.CleanupAfter {
85+
err := e2e.StopAndRemoveMCPServer(config, serverName)
86+
Expect(err).ToNot(HaveOccurred(), "Should be able to stop and remove server")
87+
}
88+
})
89+
90+
It("should build and start successfully and provide sequential_thinking tool [Serial]", func() {
91+
By("Starting the Sequential Thinking MCP server using npx:// protocol")
92+
stdout, stderr := e2e.NewTHVCommand(config, "run",
93+
"--name", serverName,
94+
"--transport", "stdio",
95+
"npx://@modelcontextprotocol/server-sequential-thinking@latest").ExpectSuccess()
96+
97+
// The command should indicate success and show build process
98+
output := stdout + stderr
99+
Expect(output).To(ContainSubstring("Building Docker image"), "Should show Docker build process")
100+
Expect(output).To(ContainSubstring("Successfully built"), "Should successfully build the image")
101+
102+
By("Waiting for the server to be running")
103+
err := e2e.WaitForMCPServer(config, serverName, 120*time.Second) // Longer timeout for protocol builds
104+
Expect(err).ToNot(HaveOccurred(), "Server should be running within 120 seconds")
105+
106+
By("Verifying the server appears in the list")
107+
stdout, _ = e2e.NewTHVCommand(config, "list").ExpectSuccess()
108+
Expect(stdout).To(ContainSubstring(serverName), "Server should appear in the list")
109+
Expect(stdout).To(ContainSubstring("running"), "Server should be in running state")
110+
Expect(stdout).To(ContainSubstring("npx-modelcontextprotocol-server-sequential-thinking"), "Should show the built image name")
111+
112+
By("Listing tools and verifying sequentialthinking tool exists")
113+
stdout, _ = e2e.NewTHVCommand(config, "mcp", "list", "tools", "--server", serverName, "--timeout", "60s").ExpectSuccess()
114+
Expect(stdout).To(ContainSubstring("sequentialthinking"), "Should find sequentialthinking tool")
115+
116+
GinkgoWriter.Printf("✅ Protocol build successful: npx://@modelcontextprotocol/server-sequential-thinking\n")
117+
GinkgoWriter.Printf("✅ Server running and provides sequential_thinking tool\n")
118+
})
119+
})
120+
121+
Context("when starting @modelcontextprotocol/[email protected]", func() {
122+
var serverName string
123+
124+
BeforeEach(func() {
125+
serverName = generateUniqueProtocolServerName("sequential-thinking-pinned-test")
126+
})
127+
128+
AfterEach(func() {
129+
if config.CleanupAfter {
130+
err := e2e.StopAndRemoveMCPServer(config, serverName)
131+
Expect(err).ToNot(HaveOccurred(), "Should be able to stop and remove server")
132+
}
133+
})
134+
135+
It("should build and start successfully and provide sequential_thinking tool [Serial]", func() {
136+
By("Starting the Sequential Thinking MCP server using npx:// protocol")
137+
stdout, stderr := e2e.NewTHVCommand(config, "run",
138+
"--name", serverName,
139+
"--transport", "stdio",
140+
"npx://@modelcontextprotocol/[email protected]").ExpectSuccess()
141+
142+
// The command should indicate success and show build process
143+
output := stdout + stderr
144+
Expect(output).To(ContainSubstring("Building Docker image"), "Should show Docker build process")
145+
Expect(output).To(ContainSubstring("Successfully built"), "Should successfully build the image")
146+
147+
By("Waiting for the server to be running")
148+
err := e2e.WaitForMCPServer(config, serverName, 120*time.Second) // Longer timeout for protocol builds
149+
Expect(err).ToNot(HaveOccurred(), "Server should be running within 120 seconds")
150+
151+
By("Verifying the server appears in the list")
152+
stdout, _ = e2e.NewTHVCommand(config, "list").ExpectSuccess()
153+
Expect(stdout).To(ContainSubstring(serverName), "Server should appear in the list")
154+
Expect(stdout).To(ContainSubstring("running"), "Server should be in running state")
155+
Expect(stdout).To(ContainSubstring("npx-modelcontextprotocol-server-sequential-thinking"), "Should show the built image name")
156+
157+
By("Listing tools and verifying sequentialthinking tool exists")
158+
stdout, _ = e2e.NewTHVCommand(config, "mcp", "list", "tools", "--server", serverName, "--timeout", "60s").ExpectSuccess()
159+
Expect(stdout).To(ContainSubstring("sequentialthinking"), "Should find sequentialthinking tool")
160+
161+
GinkgoWriter.Printf("✅ Protocol build successful: npx://@modelcontextprotocol/server-sequential-thinking\n")
162+
GinkgoWriter.Printf("✅ Server running and provides sequential_thinking tool\n")
163+
})
164+
})
165+
76166
Context("when testing error conditions", func() {
77167
var serverName string
78168

0 commit comments

Comments
 (0)