Skip to content

Commit 5e04d7f

Browse files
committed
progress
1 parent f16ea0c commit 5e04d7f

File tree

121 files changed

+6989
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+6989
-2
lines changed

epicshop/dev.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { createServer } from 'http'
2+
import { execa } from 'execa'
3+
import { getPort } from 'get-port'
4+
import { createProxyServer } from 'http-proxy'
5+
6+
const [, , ...args] = process.argv
7+
const [transport] = args
8+
9+
const serverPort = await getPort({ port: 10000, exclude: [process.env.PORT] })
10+
const clientPort = await getPort({
11+
port: 9000,
12+
exclude: [process.env.PORT, serverPort],
13+
})
14+
15+
// Spawn mcp-inspector as a sidecar process
16+
const inspectorProcess = execa('mcp-inspector', [], {
17+
env: {
18+
...process.env,
19+
SERVER_PORT: serverPort,
20+
CLIENT_PORT: clientPort,
21+
},
22+
stdio: 'inherit',
23+
})
24+
25+
const proxy = createProxyServer({
26+
target: `http://localhost:${clientPort}`,
27+
ws: true,
28+
changeOrigin: true,
29+
})
30+
31+
const server = createServer((req, res) => {
32+
if (req.url === '/' || req.url.startsWith('/?')) {
33+
// Parse the original URL and add the searchParams
34+
const transport = 'stdio'
35+
const command = 'npm'
36+
const args = `--silent --prefix ${process.cwd()} run dev:mcp`
37+
const url = new URL(req.url, `http://localhost:${clientPort}`)
38+
url.searchParams.set('transport', transport)
39+
url.searchParams.set('serverCommand', command)
40+
url.searchParams.set('serverArgs', args)
41+
42+
// Rewrite the request URL for the proxy
43+
req.url = url.pathname + url.search
44+
}
45+
proxy.web(req, res, {}, (err) => {
46+
res.writeHead(502, { 'Content-Type': 'text/plain' })
47+
res.end('Proxy error: ' + err.message)
48+
})
49+
})
50+
51+
server.on('upgrade', (req, socket, head) => {
52+
proxy.ws(req, socket, head)
53+
})
54+
55+
server.listen(process.env.PORT, () => {
56+
console.log(`Redirect server running on port ${process.env.PORT}`)
57+
})
58+
59+
// Ensure proper cleanup
60+
function cleanup() {
61+
if (inspectorProcess && !inspectorProcess.killed) {
62+
inspectorProcess.kill()
63+
}
64+
server.close(() => {
65+
console.log('HTTP server closed')
66+
})
67+
}
68+
69+
process.on('exit', cleanup)
70+
process.on('SIGINT', () => {
71+
cleanup()
72+
process.exit(0)
73+
})
74+
process.on('SIGTERM', () => {
75+
cleanup()
76+
process.exit(0)
77+
})

epicshop/mcp-dev/dev.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#!/usr/bin/env node
2+
3+
import { createServer } from 'http'
4+
import { styleText } from 'node:util'
5+
import { execa } from 'execa'
6+
import getPort from 'get-port'
7+
import httpProxy from 'http-proxy'
8+
9+
const { createProxyServer } = httpProxy
10+
11+
const [, , ...args] = process.argv
12+
const [transport] = args
13+
14+
const serverPort = await getPort({
15+
port: 10000,
16+
exclude: [process.env.PORT].filter(Boolean).map(Number),
17+
})
18+
const clientPort = await getPort({
19+
port: 9000,
20+
exclude: [process.env.PORT, serverPort].filter(Boolean).map(Number),
21+
})
22+
23+
// Spawn mcp-inspector as a sidecar process
24+
const inspectorProcess = execa('mcp-inspector', [], {
25+
env: {
26+
...process.env,
27+
SERVER_PORT: serverPort,
28+
CLIENT_PORT: clientPort,
29+
},
30+
stdio: ['inherit', 'pipe', 'inherit'], // capture stdout
31+
})
32+
33+
// Wait for the inspector to be up before starting the proxy server
34+
function waitForInspectorReady() {
35+
return new Promise((resolve) => {
36+
inspectorProcess.stdout.on('data', (data) => {
37+
const str = data.toString()
38+
// Suppress specific logs from inspector
39+
if (
40+
str.includes('Proxy server listening on port') ||
41+
str.includes('MCP Inspector is up and running')
42+
) {
43+
// Do not print these lines
44+
if (str.includes('MCP Inspector is up and running')) {
45+
resolve()
46+
}
47+
return
48+
}
49+
process.stdout.write(str) // print all other inspector logs
50+
})
51+
})
52+
}
53+
54+
await waitForInspectorReady()
55+
56+
const proxy = createProxyServer({
57+
target: `http://localhost:${clientPort}`,
58+
ws: true,
59+
changeOrigin: true,
60+
})
61+
62+
const server = createServer((req, res) => {
63+
if (req.url === '/' || req.url.startsWith('/?')) {
64+
const url = new URL(req.url, `http://localhost:${clientPort}`)
65+
const transport = 'stdio'
66+
const command = 'npm'
67+
const args = `--silent --prefix "${process.cwd()}" run dev:mcp`
68+
url.searchParams.set('transport', transport)
69+
url.searchParams.set('serverCommand', command)
70+
url.searchParams.set('serverArgs', args)
71+
const correctedUrl = url.pathname + url.search
72+
if (correctedUrl !== req.url) {
73+
res.writeHead(302, { Location: correctedUrl })
74+
res.end()
75+
return
76+
}
77+
}
78+
proxy.web(req, res, {}, (err) => {
79+
res.writeHead(502, { 'Content-Type': 'text/plain' })
80+
res.end('Proxy error: ' + err.message)
81+
})
82+
})
83+
84+
server.on('upgrade', (req, socket, head) => {
85+
proxy.ws(req, socket, head)
86+
})
87+
88+
server.listen(process.env.PORT, () => {
89+
// Enhanced, colorized logs
90+
const proxyUrl = `http://localhost:${process.env.PORT}`
91+
console.log(
92+
styleText('cyan', `🐨 Proxy server running: `) +
93+
styleText('green', proxyUrl),
94+
)
95+
console.log(
96+
styleText('gray', `- Client port: `) +
97+
styleText('magenta', clientPort.toString()),
98+
)
99+
console.log(
100+
styleText('gray', `- Server port: `) +
101+
styleText('yellow', serverPort.toString()),
102+
)
103+
})
104+
105+
// Ensure proper cleanup
106+
function cleanup() {
107+
if (inspectorProcess && !inspectorProcess.killed) {
108+
inspectorProcess.kill()
109+
}
110+
proxy.close()
111+
server.close(() => {
112+
console.log('HTTP server closed')
113+
})
114+
}
115+
116+
process.on('exit', cleanup)
117+
process.on('SIGINT', () => {
118+
cleanup()
119+
process.exit(0)
120+
})
121+
process.on('SIGTERM', () => {
122+
cleanup()
123+
process.exit(0)
124+
})

epicshop/mcp-dev/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@epic-web/mcp-dev",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"bin": {
6+
"mcp-dev": "./dev.js"
7+
}
8+
}

epicshop/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"type": "module",
33
"dependencies": {
4-
"@epic-web/workshop-app": "^5.14.5",
5-
"@epic-web/workshop-utils": "^5.14.5",
4+
"@epic-web/workshop-app": "^5.15.0",
5+
"@epic-web/workshop-utils": "^5.15.0",
66
"chokidar": "^4.0.3",
77
"enquirer": "^2.4.1",
88
"execa": "^9.5.2",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Simple Tool
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "exercises_02.tools_01.problem.simple",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "mcp-dev",
7+
"dev:mcp": "tsx src/index.ts",
8+
"test": "vitest",
9+
"typecheck": "tsc",
10+
"inspect": "mcp-inspector"
11+
},
12+
"dependencies": {
13+
"@epic-web/invariant": "^1.0.0",
14+
"@modelcontextprotocol/sdk": "^1.11.0",
15+
"zod": "^3.24.3"
16+
},
17+
"devDependencies": {
18+
"@epic-web/mcp-dev": "*",
19+
"tsx": "^4.19.3",
20+
"@epic-web/config": "^1.19.0",
21+
"@modelcontextprotocol/inspector": "^0.11.0",
22+
"@types/node": "^22.15.2",
23+
"typescript": "^5.8.3",
24+
"vitest": "^3.1.2"
25+
},
26+
"license": "GPL-3.0-only"
27+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { invariant } from '@epic-web/invariant'
2+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
4+
import { test, beforeAll, afterAll, expect } from 'vitest'
5+
6+
let client: Client
7+
8+
beforeAll(async () => {
9+
client = new Client({
10+
name: 'EpicMeTester',
11+
version: '1.0.0',
12+
})
13+
const transport = new StdioClientTransport({
14+
command: 'tsx',
15+
args: ['src/index.ts'],
16+
})
17+
await client.connect(transport)
18+
})
19+
20+
afterAll(async () => {
21+
await client.transport?.close()
22+
})
23+
24+
test('Tool Definition', async () => {
25+
const list = await client.listTools()
26+
const [firstTool] = list.tools
27+
invariant(firstTool, '🚨 No tools found')
28+
29+
expect(firstTool).toEqual(
30+
expect.objectContaining({
31+
name: expect.stringMatching(/^add$/i),
32+
description: expect.stringMatching(/^add two numbers$/i),
33+
inputSchema: expect.objectContaining({
34+
type: 'object',
35+
properties: expect.objectContaining({
36+
firstNumber: expect.objectContaining({
37+
type: 'number',
38+
description: expect.stringMatching(/first/i),
39+
}),
40+
}),
41+
}),
42+
}),
43+
)
44+
})
45+
46+
test('Tool Call', async () => {
47+
const result = await client.callTool({
48+
name: 'add',
49+
arguments: {
50+
firstNumber: 1,
51+
secondNumber: 2,
52+
},
53+
})
54+
55+
expect(result).toEqual(
56+
expect.objectContaining({
57+
content: expect.arrayContaining([
58+
expect.objectContaining({
59+
type: 'text',
60+
text: expect.stringMatching(/3/),
61+
}),
62+
]),
63+
}),
64+
)
65+
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3+
4+
const server = new McpServer(
5+
{
6+
name: 'EpicMe',
7+
version: '1.0.0',
8+
},
9+
{
10+
// 🐨 add a capabilities object with a tools property that is an empty object
11+
instructions: `
12+
EpicMe is a journaling app that allows users to write about and review their experiences, thoughts, and reflections.
13+
14+
These tools are the user's window into their journal. With these tools and your help, they can create, read, and manage their journal entries and associated tags.
15+
16+
You can also help users add tags to their entries and get all tags for an entry.
17+
`.trim(),
18+
},
19+
)
20+
21+
// 🐨 add a tool to the server with the server.tool API
22+
// - it should be named 'add'
23+
// - it should have a description explaining what it can be used to do
24+
// - provide an input schema object with two properties which are validated with zod (give them descriptions as well):
25+
// - firstNumber: a number
26+
// - secondNumber: a number
27+
// - it should return a standard text response with the sum of the two numbers
28+
29+
async function main() {
30+
const transport = new StdioServerTransport()
31+
await server.connect(transport)
32+
console.error('EpicMe MCP Server running on stdio')
33+
}
34+
35+
main().catch((error) => {
36+
console.error('Fatal error in main():', error)
37+
process.exit(1)
38+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": ["@epic-web/config/typescript"],
3+
"include": ["types/**/*.d.ts", "src/**/*.ts"]
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "@epic-web/config/reset.d.ts";

0 commit comments

Comments
 (0)