Skip to content
Open
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
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
.idea
node_modules
node_modules
dist
.build
.serverless
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ npm install
npm install -g osls
```

3. Run Locally with serverless-offline
3. Run Locally
This command compiles the TypeScript code and starts the local server using `serverless-offline`.
```bash
npm sls offline
npm run start
```

Local endpoint will be available at:
Expand Down Expand Up @@ -141,10 +142,10 @@ export const handler = middy()

## 📡 Deploy to AWS

Just run:
This command compiles the TypeScript code and deploys the service to your configured AWS account using Serverless Framework.

```bash
sls deploy
npm run deploy
```
After deployment, the MCP server will be live at the URL output by the command.

Expand Down
63 changes: 0 additions & 63 deletions __tests__/add-tool/add-tool-edge-cases.test.mjs

This file was deleted.

74 changes: 74 additions & 0 deletions __tests__/add-tool/add-tool-edge-cases.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// __tests__/mcpAddToolEdgeCases.test.ts
import { handler } from '../../src/index';
import { describe, it, expect } from '@jest/globals';
import { createMockEvent, createMockContext } from '../test-utils';

describe('MCP Server - tools/call "add" method (edge cases)', () => {

const createAddBody = (params: any, id = 999): string => {
return JSON.stringify({
jsonrpc: '2.0',
id,
method: 'tools/call',
params: {
name: 'add',
arguments: params,
},
});
};

it('should return a validation error if "a" is missing', async () => {
const body = createAddBody({ b: 2 }, 101);
const event = createMockEvent({ body, rawPath: '/add-edge-case' });
const context = createMockContext();
const response = await handler(event, context);
const responseBody = JSON.parse(response.body);
expect(responseBody).toHaveProperty('error');
expect(responseBody.error.message).toMatch(/a/i);
});

it('should return a validation error if "b" is missing', async () => {
const body = createAddBody({ a: 2 }, 102);
const event = createMockEvent({ body, rawPath: '/add-edge-case' });
const context = createMockContext();
const response = await handler(event, context);
const responseBody = JSON.parse(response.body);
expect(responseBody.error.message).toMatch(/b/i);
});

it('should return a validation error if "a" is a string', async () => {
const body = createAddBody({ a: '5', b: 2 }, 103);
const event = createMockEvent({ body, rawPath: '/add-edge-case' });
const context = createMockContext();
const response = await handler(event, context);
const responseBody = JSON.parse(response.body);
expect(responseBody.error.message).toMatch(/a/i);
});

it('should return a validation error if both are strings', async () => {
const body = createAddBody({ a: 'foo', b: 'bar' }, 104);
const event = createMockEvent({ body, rawPath: '/add-edge-case' });
const context = createMockContext();
const response = await handler(event, context);
const responseBody = JSON.parse(response.body);
expect(responseBody.error.message).toMatch(/number/i);
});

it('should return 0 when both a and b are 0', async () => {
const body = createAddBody({ a: 0, b: 0 }, 105);
const event = createMockEvent({ body, rawPath: '/add-edge-case' });
const context = createMockContext();
const response = await handler(event, context);
const responseBody = JSON.parse(response.body);
expect(responseBody.result.content[0].text).toBe('0');
});

it('should handle negative numbers correctly', async () => {
const body = createAddBody({ a: -3, b: -7 }, 106);
const event = createMockEvent({ body, rawPath: '/add-edge-case' });
const context = createMockContext();
const response = await handler(event, context);
const responseBody = JSON.parse(response.body);
expect(responseBody.result.content[0].text).toBe('-10');
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// __tests__/mcpAddTool.test.mjs
import { handler } from '../../src/index.mjs';
// __tests__/add-tool/add-tool.test.ts
import { handler } from '../../src/index';
import { describe, it, expect } from '@jest/globals';
import { createMockEvent, createMockContext } from '../test-utils';


describe('MCP Server - tools/call "add" method', () => {
it('Should return the sum of a and b', async () => {
const event = {
httpMethod: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
jsonrpc: '2.0',
},

const event = createMockEvent({
rawPath: '/add-tool',
body: JSON.stringify({
jsonrpc: '2.0',
id: 2,
Expand All @@ -22,12 +21,9 @@ describe('MCP Server - tools/call "add" method', () => {
},
},
}),
};

const context = {};

});
const context = createMockContext();
const response = await handler(event, context);

expect(response.statusCode).toBe(200);

const body = JSON.parse(response.body);
Expand All @@ -43,4 +39,4 @@ describe('MCP Server - tools/call "add" method', () => {
text: '8',
});
});
});
});
70 changes: 70 additions & 0 deletions __tests__/test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { APIGatewayProxyEventV2, Context } from 'aws-lambda';
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';

type HandlerContext = Context & { jsonRPCMessages: JSONRPCMessage[] };

/**
* Creates a mock APIGatewayProxyEventV2 object for testing.
* Allows overriding specific properties.
*/
export function createMockEvent(overrides: Partial<APIGatewayProxyEventV2> = {}): APIGatewayProxyEventV2 {
const defaultEvent: APIGatewayProxyEventV2 = {
version: '2.0',
routeKey: '$default',
rawPath: '/test',
rawQueryString: '',
headers: {
'content-type': 'application/json',
accept: 'application/json',
jsonrpc: '2.0',
...overrides.headers,
},
requestContext: {
accountId: '123456789012',
apiId: 'api-id',
domainName: 'id.execute-api.us-east-1.amazonaws.com',
domainPrefix: 'id',
http: {
method: 'POST',
path: '/test',
protocol: 'HTTP/1.1',
sourceIp: '127.0.0.1',
userAgent: 'Test Agent',
},
requestId: 'request-id',
routeKey: '$default',
stage: '$default',
time: '01/Mar/2020:00:00:00 +0000',
timeEpoch: 1583011200000,
...(overrides.requestContext as any),
},
body: '{}',
isBase64Encoded: false,
...overrides,
};
return defaultEvent;
}

/**
* Creates a mock Lambda Context object for testing.
* Includes the jsonRPCMessages property expected by the middleware.
*/
export function createMockContext(overrides: Partial<HandlerContext> = {}): HandlerContext {
const defaultContext: HandlerContext = {
callbackWaitsForEmptyEventLoop: true,
functionName: 'test-function',
functionVersion: '$LATEST',
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:test-function',
memoryLimitInMB: '128',
awsRequestId: 'test-request-id',
logGroupName: '/aws/lambda/test-function',
logStreamName: '2023/01/01/[$LATEST]abcdef1234567890',
getRemainingTimeInMillis: () => 5 * 60 * 1000,
done: () => {},
fail: () => {},
succeed: () => {},
jsonRPCMessages: [],
...overrides,
};
return defaultContext;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { handler } from '../../src/index.mjs';
// __tests__/tool-list/tool-list.test.ts
import { handler } from '../../src/index';
import { describe, it, expect } from '@jest/globals';
import { createMockEvent, createMockContext } from '../test-utils';

describe('MCP Server - tools/list method', () => {
it('Should return a list of tools', async () => {
const event = {
httpMethod: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
jsonrpc: '2.0',
},
const event = createMockEvent({
rawPath: '/list-tools',
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
id: 1,
}),
};
});

const context = {}; // Lambda context (empty for unit tests)
const context = createMockContext();

const response = await handler(event, context);

Expand Down
8 changes: 0 additions & 8 deletions jest.config.mjs

This file was deleted.

26 changes: 26 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { JestConfigWithTsJest } from 'ts-jest';

const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\.{1,2}/.*)\.js$': '$1',
},
transform: {
'^.+\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
transformIgnorePatterns: [
'/node_modules/(?!(@middy)/)',
],
testPathIgnorePatterns: [
'<rootDir>/__tests__/test-utils.ts',
],
};

export default config;
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
{
"name": "serverless-mcp-server",
"version": "1.0.0",
"version": "1.0.1",
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules ./node_modules/.bin/jest"
"start": "tsc && serverless offline start",
"deploy": "tsc && serverless deploy --force",
"test": "NODE_OPTIONS=--experimental-vm-modules npx jest"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@types/aws-lambda": "^8.10.148",
"@types/http-errors": "^2.0.4",
"@types/jest": "^29.5.14",
"@types/node": "^22.14.0",
"jest": "^29.7.0",
"serverless-offline": "^14.4.0"
"serverless-offline": "^13.3.4",
"serverless-plugin-typescript": "^2.1.5",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"@middy/core": "^6.1.6",
Expand Down
Loading