Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: '22'
cache: 'npm'

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [18, 20]
node-version: [18, 20, 22, 23]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18
v22
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ This demo showcases how to build sophisticated AI agents using MCP over MQTT for
- 🔍 **Auto Discovery**: Automatic server discovery over MQTT topics
- 🌍 **Environment Detection**: Automatic browser/Node.js detection with appropriate defaults

## Requirements

- Node.js >= 18

## Installation

```bash
Expand Down
12 changes: 7 additions & 5 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,20 @@ export function createNotification(method: string, params?: Record<string, any>)
}

export function isRequest(message: any): message is JSONRPCRequest {
return message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && 'id' in message
return Boolean(
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && 'id' in message,
)
}

export function isResponse(message: any): message is JSONRPCResponse {
return (
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'id' in message && !('method' in message)
return Boolean(
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'id' in message && !('method' in message),
)
}

export function isNotification(message: any): message is JSONRPCNotification {
return (
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && !('id' in message)
return Boolean(
message && typeof message === 'object' && message.jsonrpc === '2.0' && 'method' in message && !('id' in message),
)
}

Expand Down
193 changes: 193 additions & 0 deletions test/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { describe, it, expect } from '@jest/globals'
import {
JSONRPCRequestSchema,
JSONRPCResponseSchema,
JSONRPCNotificationSchema,
ToolSchema,
ResourceSchema,
ErrorCode,
} from '../src/types.js'

describe('Types and Schemas', () => {
describe('JSONRPCRequestSchema', () => {
it('should validate a valid request', () => {
const request = {
jsonrpc: '2.0',
id: 'test-123',
method: 'tools/call',
params: { name: 'test-tool' },
}
const result = JSONRPCRequestSchema.safeParse(request)
expect(result.success).toBe(true)
})

it('should validate request with numeric id', () => {
const request = {
jsonrpc: '2.0',
id: 123,
method: 'test',
}
const result = JSONRPCRequestSchema.safeParse(request)
expect(result.success).toBe(true)
})

it('should reject invalid jsonrpc version', () => {
const request = {
jsonrpc: '1.0',
id: 'test',
method: 'test',
}
const result = JSONRPCRequestSchema.safeParse(request)
expect(result.success).toBe(false)
})

it('should reject request without method', () => {
const request = {
jsonrpc: '2.0',
id: 'test',
}
const result = JSONRPCRequestSchema.safeParse(request)
expect(result.success).toBe(false)
})
})

describe('JSONRPCResponseSchema', () => {
it('should validate a successful response', () => {
const response = {
jsonrpc: '2.0',
id: 'test-123',
result: { data: 'success' },
}
const result = JSONRPCResponseSchema.safeParse(response)
expect(result.success).toBe(true)
})

it('should validate an error response', () => {
const response = {
jsonrpc: '2.0',
id: 'test-123',
error: {
code: ErrorCode.INVALID_PARAMS,
message: 'Invalid parameters',
},
}
const result = JSONRPCResponseSchema.safeParse(response)
expect(result.success).toBe(true)
})

it('should validate error response with data', () => {
const response = {
jsonrpc: '2.0',
id: 'test-123',
error: {
code: ErrorCode.INTERNAL_ERROR,
message: 'Internal error',
data: { details: 'More info' },
},
}
const result = JSONRPCResponseSchema.safeParse(response)
expect(result.success).toBe(true)
})
})

describe('JSONRPCNotificationSchema', () => {
it('should validate a valid notification', () => {
const notification = {
jsonrpc: '2.0',
method: 'notifications/server/online',
params: { server_name: 'test' },
}
const result = JSONRPCNotificationSchema.safeParse(notification)
expect(result.success).toBe(true)
})

it('should validate notification without params', () => {
const notification = {
jsonrpc: '2.0',
method: 'notifications/disconnected',
}
const result = JSONRPCNotificationSchema.safeParse(notification)
expect(result.success).toBe(true)
})
})

describe('ToolSchema', () => {
it('should validate a valid tool', () => {
const tool = {
name: 'test-tool',
description: 'A test tool',
inputSchema: {
type: 'object',
properties: {
input: { type: 'string' },
},
},
}
const result = ToolSchema.safeParse(tool)
expect(result.success).toBe(true)
})

it('should validate tool without description', () => {
const tool = {
name: 'minimal-tool',
inputSchema: {},
}
const result = ToolSchema.safeParse(tool)
expect(result.success).toBe(true)
})

it('should reject tool without name', () => {
const tool = {
inputSchema: {},
}
const result = ToolSchema.safeParse(tool)
expect(result.success).toBe(false)
})
})

describe('ResourceSchema', () => {
it('should validate a valid resource', () => {
const resource = {
uri: 'file:///path/to/resource',
name: 'Test Resource',
description: 'A test resource',
mimeType: 'text/plain',
}
const result = ResourceSchema.safeParse(resource)
expect(result.success).toBe(true)
})

it('should validate resource with minimal fields', () => {
const resource = {
uri: 'file:///path',
name: 'Minimal',
}
const result = ResourceSchema.safeParse(resource)
expect(result.success).toBe(true)
})

it('should reject resource without uri', () => {
const resource = {
name: 'No URI',
}
const result = ResourceSchema.safeParse(resource)
expect(result.success).toBe(false)
})
})

describe('ErrorCode', () => {
it('should have correct JSON-RPC error codes', () => {
expect(ErrorCode.PARSE_ERROR).toBe(-32700)
expect(ErrorCode.INVALID_REQUEST).toBe(-32600)
expect(ErrorCode.METHOD_NOT_FOUND).toBe(-32601)
expect(ErrorCode.INVALID_PARAMS).toBe(-32602)
expect(ErrorCode.INTERNAL_ERROR).toBe(-32603)
})

it('should have correct MCP-specific error codes', () => {
expect(ErrorCode.INVALID_MESSAGE).toBe(-32000)
expect(ErrorCode.TOOL_NOT_FOUND).toBe(-32001)
expect(ErrorCode.RESOURCE_NOT_FOUND).toBe(-32002)
})
})
})
125 changes: 124 additions & 1 deletion test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mock-id-123'),
}))

import { createRequest, createResponse, createNotification, generateId, McpError } from '../src/shared/utils.js'
import {
createRequest,
createResponse,
createNotification,
generateId,
McpError,
isRequest,
isResponse,
isNotification,
isNode,
isBrowser,
} from '../src/shared/utils.js'
import { ErrorCode } from '../src/types.js'

describe('Utils', () => {
Expand Down Expand Up @@ -94,5 +105,117 @@ describe('Utils', () => {
throw new McpError(ErrorCode.INTERNAL_ERROR, 'Something went wrong')
}).toThrow(McpError)
})

it('should serialize to JSON correctly', () => {
const error = new McpError(ErrorCode.INVALID_PARAMS, 'Invalid parameters', { field: 'name' })
const json = error.toJSON()

expect(json).toEqual({
code: ErrorCode.INVALID_PARAMS,
message: 'Invalid parameters',
data: { field: 'name' },
})
})

it('should serialize to JSON without data when not provided', () => {
const error = new McpError(ErrorCode.INTERNAL_ERROR, 'Internal error')
const json = error.toJSON()

expect(json).toEqual({
code: ErrorCode.INTERNAL_ERROR,
message: 'Internal error',
})
expect('data' in json).toBe(false)
})
})

describe('isRequest', () => {
it('should return true for valid request', () => {
const request = { jsonrpc: '2.0', id: '123', method: 'test' }
expect(isRequest(request)).toBe(true)
})

it('should return false for response', () => {
const response = { jsonrpc: '2.0', id: '123', result: {} }
expect(isRequest(response)).toBe(false)
})

it('should return false for notification', () => {
const notification = { jsonrpc: '2.0', method: 'test' }
expect(isRequest(notification)).toBe(false)
})

it('should return false for invalid objects', () => {
expect(isRequest(null)).toBe(false)
expect(isRequest(undefined)).toBe(false)
expect(isRequest({})).toBe(false)
expect(isRequest({ jsonrpc: '1.0', id: '1', method: 'test' })).toBe(false)
})
})

describe('isResponse', () => {
it('should return true for valid response with result', () => {
const response = { jsonrpc: '2.0', id: '123', result: { data: 'test' } }
expect(isResponse(response)).toBe(true)
})

it('should return true for valid response with error', () => {
const response = { jsonrpc: '2.0', id: '123', error: { code: -32600, message: 'Invalid' } }
expect(isResponse(response)).toBe(true)
})

it('should return false for request', () => {
const request = { jsonrpc: '2.0', id: '123', method: 'test' }
expect(isResponse(request)).toBe(false)
})

it('should return false for notification', () => {
const notification = { jsonrpc: '2.0', method: 'test' }
expect(isResponse(notification)).toBe(false)
})

it('should return false for invalid objects', () => {
expect(isResponse(null)).toBe(false)
expect(isResponse(undefined)).toBe(false)
expect(isResponse({})).toBe(false)
})
})

describe('isNotification', () => {
it('should return true for valid notification', () => {
const notification = { jsonrpc: '2.0', method: 'test' }
expect(isNotification(notification)).toBe(true)
})

it('should return true for notification with params', () => {
const notification = { jsonrpc: '2.0', method: 'test', params: { key: 'value' } }
expect(isNotification(notification)).toBe(true)
})

it('should return false for request', () => {
const request = { jsonrpc: '2.0', id: '123', method: 'test' }
expect(isNotification(request)).toBe(false)
})

it('should return false for response', () => {
const response = { jsonrpc: '2.0', id: '123', result: {} }
expect(isNotification(response)).toBe(false)
})

it('should return false for invalid objects', () => {
expect(isNotification(null)).toBe(false)
expect(isNotification(undefined)).toBe(false)
expect(isNotification({})).toBe(false)
})
})

describe('Environment detection', () => {
it('should detect Node.js environment', () => {
expect(isNode()).toBe(true)
})

it('should not detect browser environment in Node.js', () => {
expect(isBrowser()).toBe(false)
})
})
})