Skip to content

Commit ab23dd2

Browse files
committed
finished
1 parent 5a3b9d1 commit ab23dd2

File tree

107 files changed

+10914
-14
lines changed

Some content is hidden

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

107 files changed

+10914
-14
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# List Changed

exercises/99.finished/01.solution.finished/other/caveat-variable-font.ttf renamed to exercises/05.changes/01.problem.list-changed/other/caveat-variable-font.ttf

File renamed without changes.

exercises/99.finished/01.solution.finished/package.json renamed to exercises/05.changes/01.problem.list-changed/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "exercises_99.finished_01.solution.finished",
2+
"name": "exercises_05.changes_01.problem.list-changed",
33
"private": true,
44
"type": "module",
55
"scripts": {

exercises/99.finished/01.solution.finished/src/db/index.ts renamed to exercises/05.changes/01.problem.list-changed/src/db/index.ts

File renamed without changes.

exercises/99.finished/01.solution.finished/src/db/migrations.ts renamed to exercises/05.changes/01.problem.list-changed/src/db/migrations.ts

File renamed without changes.

exercises/99.finished/01.solution.finished/src/db/schema.ts renamed to exercises/05.changes/01.problem.list-changed/src/db/schema.ts

File renamed without changes.

exercises/99.finished/01.solution.finished/src/db/seed.ts renamed to exercises/05.changes/01.problem.list-changed/src/db/seed.ts

File renamed without changes.

exercises/99.finished/01.solution.finished/src/db/utils.ts renamed to exercises/05.changes/01.problem.list-changed/src/db/utils.ts

File renamed without changes.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
3+
import { invariant } from '@epic-web/invariant'
4+
import { faker } from '@faker-js/faker'
5+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
6+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
7+
import {
8+
CreateMessageRequestSchema,
9+
type CreateMessageResult,
10+
} from '@modelcontextprotocol/sdk/types.js'
11+
import { test, beforeAll, afterAll, expect } from 'vitest'
12+
import { type z } from 'zod'
13+
14+
let client: Client
15+
const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite`
16+
17+
beforeAll(async () => {
18+
const dir = path.dirname(EPIC_ME_DB_PATH)
19+
await fs.mkdir(dir, { recursive: true })
20+
client = new Client(
21+
{
22+
name: 'EpicMeTester',
23+
version: '1.0.0',
24+
},
25+
{
26+
capabilities: {
27+
sampling: {},
28+
},
29+
},
30+
)
31+
const transport = new StdioClientTransport({
32+
command: 'tsx',
33+
args: ['src/index.ts'],
34+
env: {
35+
...process.env,
36+
EPIC_ME_DB_PATH,
37+
},
38+
})
39+
await client.connect(transport)
40+
})
41+
42+
afterAll(async () => {
43+
await client.transport?.close()
44+
await fs.unlink(EPIC_ME_DB_PATH)
45+
})
46+
47+
test('Tool Definition', async () => {
48+
const list = await client.listTools()
49+
const [firstTool] = list.tools
50+
invariant(firstTool, '🚨 No tools found')
51+
52+
expect(firstTool).toEqual(
53+
expect.objectContaining({
54+
name: expect.stringMatching(/^create_entry$/i),
55+
description: expect.stringMatching(/^create a new journal entry$/i),
56+
inputSchema: expect.objectContaining({
57+
type: 'object',
58+
properties: expect.objectContaining({
59+
title: expect.objectContaining({
60+
type: 'string',
61+
description: expect.stringMatching(/title/i),
62+
}),
63+
content: expect.objectContaining({
64+
type: 'string',
65+
description: expect.stringMatching(/content/i),
66+
}),
67+
}),
68+
}),
69+
}),
70+
)
71+
})
72+
73+
async function deferred<ResolvedValue>() {
74+
const ref = {} as {
75+
promise: Promise<ResolvedValue>
76+
resolve: (value: ResolvedValue) => void
77+
reject: (reason?: any) => void
78+
value: ResolvedValue | undefined
79+
reason: any | undefined
80+
}
81+
ref.promise = new Promise<ResolvedValue>((resolve, reject) => {
82+
ref.resolve = (value) => {
83+
ref.value = value
84+
resolve(value)
85+
}
86+
ref.reject = (reason) => {
87+
ref.reason = reason
88+
reject(reason)
89+
}
90+
})
91+
92+
return ref
93+
}
94+
95+
test('Sampling', async () => {
96+
const messageResultDeferred = await deferred<CreateMessageResult>()
97+
const messageRequestDeferred =
98+
await deferred<z.infer<typeof CreateMessageRequestSchema>>()
99+
100+
client.setRequestHandler(CreateMessageRequestSchema, (r) => {
101+
messageRequestDeferred.resolve(r)
102+
return messageResultDeferred.promise
103+
})
104+
105+
const fakeTag1 = {
106+
name: faker.lorem.word(),
107+
description: faker.lorem.sentence(),
108+
}
109+
const fakeTag2 = {
110+
name: faker.lorem.word(),
111+
description: faker.lorem.sentence(),
112+
}
113+
114+
const result = await client.callTool({
115+
name: 'create_tag',
116+
arguments: fakeTag1,
117+
})
118+
const tag1Resource = (result.content as any).find(
119+
(c: any) => c.type === 'resource',
120+
)?.resource
121+
invariant(tag1Resource, '🚨 No tag1 resource found')
122+
const newTag1 = JSON.parse(tag1Resource.text) as any
123+
invariant(newTag1.id, '🚨 No new tag1 found')
124+
125+
const entry = {
126+
title: faker.lorem.words(3),
127+
content: faker.lorem.paragraphs(2),
128+
}
129+
await client.callTool({
130+
name: 'create_entry',
131+
arguments: entry,
132+
})
133+
const request = await messageRequestDeferred.promise
134+
135+
expect(request).toEqual(
136+
expect.objectContaining({
137+
method: 'sampling/createMessage',
138+
params: expect.objectContaining({
139+
maxTokens: expect.any(Number),
140+
systemPrompt: expect.stringMatching(/example/i),
141+
messages: expect.arrayContaining([
142+
expect.objectContaining({
143+
role: 'user',
144+
content: expect.objectContaining({
145+
type: 'text',
146+
text: expect.stringMatching(/entry/i),
147+
mimeType: 'application/json',
148+
}),
149+
}),
150+
]),
151+
}),
152+
}),
153+
)
154+
155+
messageResultDeferred.resolve({
156+
model: 'stub-model',
157+
stopReason: 'endTurn',
158+
role: 'assistant',
159+
content: {
160+
type: 'text',
161+
text: JSON.stringify([{ id: newTag1.id }, fakeTag2]),
162+
},
163+
})
164+
165+
// give the server a chance to process the result
166+
await new Promise((resolve) => setTimeout(resolve, 100))
167+
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3+
import { DB } from './db/index.ts'
4+
import { initializePrompts } from './prompts.ts'
5+
import { initializeResources } from './resources.ts'
6+
import { initializeTools } from './tools.ts'
7+
8+
export class EpicMeMCP {
9+
db: DB
10+
server = new McpServer(
11+
{
12+
name: 'EpicMe',
13+
version: '1.0.0',
14+
},
15+
{
16+
capabilities: {
17+
tools: {},
18+
resources: {},
19+
completions: {},
20+
prompts: {},
21+
},
22+
instructions: `
23+
EpicMe is a journaling app that allows users to write about and review their experiences, thoughts, and reflections.
24+
25+
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.
26+
27+
You can also help users add tags to their entries and get all tags for an entry.
28+
`.trim(),
29+
},
30+
)
31+
32+
constructor(path: string) {
33+
this.db = DB.getInstance(path)
34+
}
35+
async init() {
36+
await initializeTools(this)
37+
await initializeResources(this)
38+
await initializePrompts(this)
39+
}
40+
}
41+
42+
async function main() {
43+
const agent = new EpicMeMCP(process.env.EPIC_ME_DB_PATH ?? './db.sqlite')
44+
await agent.init()
45+
const transport = new StdioServerTransport()
46+
await agent.server.connect(transport)
47+
console.error('EpicMe MCP Server running on stdio')
48+
}
49+
50+
main().catch((error) => {
51+
console.error('Fatal error in main():', error)
52+
process.exit(1)
53+
})

0 commit comments

Comments
 (0)