Skip to content

Commit 72e3cae

Browse files
authored
Merge pull request #2 from emqx/chore/upgrade-node-22-and-add-tests
chore: upgrade Node.js to v22 for development and expand test coverage
2 parents ae17381 + 8e86ffa commit 72e3cae

File tree

7 files changed

+331
-9
lines changed

7 files changed

+331
-9
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Node.js
1818
uses: actions/setup-node@v4
1919
with:
20-
node-version: '18'
20+
node-version: '22'
2121
cache: 'npm'
2222

2323
- name: Install dependencies

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
strategy:
1515
matrix:
16-
node-version: [18, 20]
16+
node-version: [18, 20, 22, 23]
1717

1818
steps:
1919
- uses: actions/checkout@v4

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v18
1+
v22

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ This demo showcases how to build sophisticated AI agents using MCP over MQTT for
2424
- 🔍 **Auto Discovery**: Automatic server discovery over MQTT topics
2525
- 🌍 **Environment Detection**: Automatic browser/Node.js detection with appropriate defaults
2626

27+
## Requirements
28+
29+
- Node.js >= 18
30+
2731
## Installation
2832

2933
```bash

src/shared/utils.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,20 @@ export function createNotification(method: string, params?: Record<string, any>)
3636
}
3737

3838
export function isRequest(message: any): message is JSONRPCRequest {
39-
return message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && 'id' in message
39+
return Boolean(
40+
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && 'id' in message,
41+
)
4042
}
4143

4244
export function isResponse(message: any): message is JSONRPCResponse {
43-
return (
44-
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'id' in message && !('method' in message)
45+
return Boolean(
46+
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'id' in message && !('method' in message),
4547
)
4648
}
4749

4850
export function isNotification(message: any): message is JSONRPCNotification {
49-
return (
50-
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && !('id' in message)
51+
return Boolean(
52+
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && !('id' in message),
5153
)
5254
}
5355

test/types.test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { describe, it, expect } from '@jest/globals'
2+
import {
3+
JSONRPCRequestSchema,
4+
JSONRPCResponseSchema,
5+
JSONRPCNotificationSchema,
6+
ToolSchema,
7+
ResourceSchema,
8+
ErrorCode,
9+
} from '../src/types.js'
10+
11+
describe('Types and Schemas', () => {
12+
describe('JSONRPCRequestSchema', () => {
13+
it('should validate a valid request', () => {
14+
const request = {
15+
jsonrpc: '2.0',
16+
id: 'test-123',
17+
method: 'tools/call',
18+
params: { name: 'test-tool' },
19+
}
20+
const result = JSONRPCRequestSchema.safeParse(request)
21+
expect(result.success).toBe(true)
22+
})
23+
24+
it('should validate request with numeric id', () => {
25+
const request = {
26+
jsonrpc: '2.0',
27+
id: 123,
28+
method: 'test',
29+
}
30+
const result = JSONRPCRequestSchema.safeParse(request)
31+
expect(result.success).toBe(true)
32+
})
33+
34+
it('should reject invalid jsonrpc version', () => {
35+
const request = {
36+
jsonrpc: '1.0',
37+
id: 'test',
38+
method: 'test',
39+
}
40+
const result = JSONRPCRequestSchema.safeParse(request)
41+
expect(result.success).toBe(false)
42+
})
43+
44+
it('should reject request without method', () => {
45+
const request = {
46+
jsonrpc: '2.0',
47+
id: 'test',
48+
}
49+
const result = JSONRPCRequestSchema.safeParse(request)
50+
expect(result.success).toBe(false)
51+
})
52+
})
53+
54+
describe('JSONRPCResponseSchema', () => {
55+
it('should validate a successful response', () => {
56+
const response = {
57+
jsonrpc: '2.0',
58+
id: 'test-123',
59+
result: { data: 'success' },
60+
}
61+
const result = JSONRPCResponseSchema.safeParse(response)
62+
expect(result.success).toBe(true)
63+
})
64+
65+
it('should validate an error response', () => {
66+
const response = {
67+
jsonrpc: '2.0',
68+
id: 'test-123',
69+
error: {
70+
code: ErrorCode.INVALID_PARAMS,
71+
message: 'Invalid parameters',
72+
},
73+
}
74+
const result = JSONRPCResponseSchema.safeParse(response)
75+
expect(result.success).toBe(true)
76+
})
77+
78+
it('should validate error response with data', () => {
79+
const response = {
80+
jsonrpc: '2.0',
81+
id: 'test-123',
82+
error: {
83+
code: ErrorCode.INTERNAL_ERROR,
84+
message: 'Internal error',
85+
data: { details: 'More info' },
86+
},
87+
}
88+
const result = JSONRPCResponseSchema.safeParse(response)
89+
expect(result.success).toBe(true)
90+
})
91+
})
92+
93+
describe('JSONRPCNotificationSchema', () => {
94+
it('should validate a valid notification', () => {
95+
const notification = {
96+
jsonrpc: '2.0',
97+
method: 'notifications/server/online',
98+
params: { server_name: 'test' },
99+
}
100+
const result = JSONRPCNotificationSchema.safeParse(notification)
101+
expect(result.success).toBe(true)
102+
})
103+
104+
it('should validate notification without params', () => {
105+
const notification = {
106+
jsonrpc: '2.0',
107+
method: 'notifications/disconnected',
108+
}
109+
const result = JSONRPCNotificationSchema.safeParse(notification)
110+
expect(result.success).toBe(true)
111+
})
112+
})
113+
114+
describe('ToolSchema', () => {
115+
it('should validate a valid tool', () => {
116+
const tool = {
117+
name: 'test-tool',
118+
description: 'A test tool',
119+
inputSchema: {
120+
type: 'object',
121+
properties: {
122+
input: { type: 'string' },
123+
},
124+
},
125+
}
126+
const result = ToolSchema.safeParse(tool)
127+
expect(result.success).toBe(true)
128+
})
129+
130+
it('should validate tool without description', () => {
131+
const tool = {
132+
name: 'minimal-tool',
133+
inputSchema: {},
134+
}
135+
const result = ToolSchema.safeParse(tool)
136+
expect(result.success).toBe(true)
137+
})
138+
139+
it('should reject tool without name', () => {
140+
const tool = {
141+
inputSchema: {},
142+
}
143+
const result = ToolSchema.safeParse(tool)
144+
expect(result.success).toBe(false)
145+
})
146+
})
147+
148+
describe('ResourceSchema', () => {
149+
it('should validate a valid resource', () => {
150+
const resource = {
151+
uri: 'file:///path/to/resource',
152+
name: 'Test Resource',
153+
description: 'A test resource',
154+
mimeType: 'text/plain',
155+
}
156+
const result = ResourceSchema.safeParse(resource)
157+
expect(result.success).toBe(true)
158+
})
159+
160+
it('should validate resource with minimal fields', () => {
161+
const resource = {
162+
uri: 'file:///path',
163+
name: 'Minimal',
164+
}
165+
const result = ResourceSchema.safeParse(resource)
166+
expect(result.success).toBe(true)
167+
})
168+
169+
it('should reject resource without uri', () => {
170+
const resource = {
171+
name: 'No URI',
172+
}
173+
const result = ResourceSchema.safeParse(resource)
174+
expect(result.success).toBe(false)
175+
})
176+
})
177+
178+
describe('ErrorCode', () => {
179+
it('should have correct JSON-RPC error codes', () => {
180+
expect(ErrorCode.PARSE_ERROR).toBe(-32700)
181+
expect(ErrorCode.INVALID_REQUEST).toBe(-32600)
182+
expect(ErrorCode.METHOD_NOT_FOUND).toBe(-32601)
183+
expect(ErrorCode.INVALID_PARAMS).toBe(-32602)
184+
expect(ErrorCode.INTERNAL_ERROR).toBe(-32603)
185+
})
186+
187+
it('should have correct MCP-specific error codes', () => {
188+
expect(ErrorCode.INVALID_MESSAGE).toBe(-32000)
189+
expect(ErrorCode.TOOL_NOT_FOUND).toBe(-32001)
190+
expect(ErrorCode.RESOURCE_NOT_FOUND).toBe(-32002)
191+
})
192+
})
193+
})

test/utils.test.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@ jest.mock('nanoid', () => ({
55
nanoid: jest.fn(() => 'mock-id-123'),
66
}))
77

8-
import { createRequest, createResponse, createNotification, generateId, McpError } from '../src/shared/utils.js'
8+
import {
9+
createRequest,
10+
createResponse,
11+
createNotification,
12+
generateId,
13+
McpError,
14+
isRequest,
15+
isResponse,
16+
isNotification,
17+
isNode,
18+
isBrowser,
19+
} from '../src/shared/utils.js'
920
import { ErrorCode } from '../src/types.js'
1021

1122
describe('Utils', () => {
@@ -94,5 +105,117 @@ describe('Utils', () => {
94105
throw new McpError(ErrorCode.INTERNAL_ERROR, 'Something went wrong')
95106
}).toThrow(McpError)
96107
})
108+
109+
it('should serialize to JSON correctly', () => {
110+
const error = new McpError(ErrorCode.INVALID_PARAMS, 'Invalid parameters', { field: 'name' })
111+
const json = error.toJSON()
112+
113+
expect(json).toEqual({
114+
code: ErrorCode.INVALID_PARAMS,
115+
message: 'Invalid parameters',
116+
data: { field: 'name' },
117+
})
118+
})
119+
120+
it('should serialize to JSON without data when not provided', () => {
121+
const error = new McpError(ErrorCode.INTERNAL_ERROR, 'Internal error')
122+
const json = error.toJSON()
123+
124+
expect(json).toEqual({
125+
code: ErrorCode.INTERNAL_ERROR,
126+
message: 'Internal error',
127+
})
128+
expect('data' in json).toBe(false)
129+
})
130+
})
131+
132+
describe('isRequest', () => {
133+
it('should return true for valid request', () => {
134+
const request = { jsonrpc: '2.0', id: '123', method: 'test' }
135+
expect(isRequest(request)).toBe(true)
136+
})
137+
138+
it('should return false for response', () => {
139+
const response = { jsonrpc: '2.0', id: '123', result: {} }
140+
expect(isRequest(response)).toBe(false)
141+
})
142+
143+
it('should return false for notification', () => {
144+
const notification = { jsonrpc: '2.0', method: 'test' }
145+
expect(isRequest(notification)).toBe(false)
146+
})
147+
148+
it('should return false for invalid objects', () => {
149+
expect(isRequest(null)).toBe(false)
150+
expect(isRequest(undefined)).toBe(false)
151+
expect(isRequest({})).toBe(false)
152+
expect(isRequest({ jsonrpc: '1.0', id: '1', method: 'test' })).toBe(false)
153+
})
154+
})
155+
156+
describe('isResponse', () => {
157+
it('should return true for valid response with result', () => {
158+
const response = { jsonrpc: '2.0', id: '123', result: { data: 'test' } }
159+
expect(isResponse(response)).toBe(true)
160+
})
161+
162+
it('should return true for valid response with error', () => {
163+
const response = { jsonrpc: '2.0', id: '123', error: { code: -32600, message: 'Invalid' } }
164+
expect(isResponse(response)).toBe(true)
165+
})
166+
167+
it('should return false for request', () => {
168+
const request = { jsonrpc: '2.0', id: '123', method: 'test' }
169+
expect(isResponse(request)).toBe(false)
170+
})
171+
172+
it('should return false for notification', () => {
173+
const notification = { jsonrpc: '2.0', method: 'test' }
174+
expect(isResponse(notification)).toBe(false)
175+
})
176+
177+
it('should return false for invalid objects', () => {
178+
expect(isResponse(null)).toBe(false)
179+
expect(isResponse(undefined)).toBe(false)
180+
expect(isResponse({})).toBe(false)
181+
})
182+
})
183+
184+
describe('isNotification', () => {
185+
it('should return true for valid notification', () => {
186+
const notification = { jsonrpc: '2.0', method: 'test' }
187+
expect(isNotification(notification)).toBe(true)
188+
})
189+
190+
it('should return true for notification with params', () => {
191+
const notification = { jsonrpc: '2.0', method: 'test', params: { key: 'value' } }
192+
expect(isNotification(notification)).toBe(true)
193+
})
194+
195+
it('should return false for request', () => {
196+
const request = { jsonrpc: '2.0', id: '123', method: 'test' }
197+
expect(isNotification(request)).toBe(false)
198+
})
199+
200+
it('should return false for response', () => {
201+
const response = { jsonrpc: '2.0', id: '123', result: {} }
202+
expect(isNotification(response)).toBe(false)
203+
})
204+
205+
it('should return false for invalid objects', () => {
206+
expect(isNotification(null)).toBe(false)
207+
expect(isNotification(undefined)).toBe(false)
208+
expect(isNotification({})).toBe(false)
209+
})
210+
})
211+
212+
describe('Environment detection', () => {
213+
it('should detect Node.js environment', () => {
214+
expect(isNode()).toBe(true)
215+
})
216+
217+
it('should not detect browser environment in Node.js', () => {
218+
expect(isBrowser()).toBe(false)
219+
})
97220
})
98221
})

0 commit comments

Comments
 (0)