diff --git a/README.md b/README.md index bde1b86..8035bec 100644 --- a/README.md +++ b/README.md @@ -378,6 +378,7 @@ With different `USE_API_URL_PREFIX` settings: - **Server Management**: Start, stop, rebuild, and check endpoints - **Endpoint Creation**: Generate new API endpoints via LLM +- **Media Creation**: Ask the LLM to Generate images and videos to serve locally ![mcp-1](images/mcp-1.png) @@ -385,6 +386,8 @@ With different `USE_API_URL_PREFIX` settings: ![mcp-3](images/mcp-3.png) +![mcp-4](images/mcp-4.png) + ### Debugging MCP Test the MCP server with the inspector: diff --git a/cypress/e2e/demo-endpoints-spec.cy.ts b/cypress/e2e/demo-endpoints-spec.cy.ts index e5d0506..df51a77 100644 --- a/cypress/e2e/demo-endpoints-spec.cy.ts +++ b/cypress/e2e/demo-endpoints-spec.cy.ts @@ -1,186 +1,232 @@ const catsSchema = { - $schema: 'http://json-schema.org/draft-04/schema#', - type: 'array', - items: [ - { - type: 'object', - properties: { - id: { - type: 'integer', - }, - type: { - type: 'string', - }, - name: { - type: 'string', - }, - starSign: { - type: 'string', - }, - }, - required: ['id', 'type', 'name', 'starSign'], - }, - { - type: 'object', - properties: { - id: { - type: 'integer', - }, - type: { - type: 'string', - }, - name: { - type: 'string', - }, - starSign: { - type: 'string', - }, - }, - required: ['id', 'type', 'name', 'starSign'], - }, - { - type: 'object', - properties: { - id: { - type: 'integer', - }, - type: { - type: 'string', - }, - name: { - type: 'string', - }, - starSign: { - type: 'string', - }, - }, - required: ['id', 'type', 'name', 'starSign'], - }, - { - type: 'object', - properties: { - id: { - type: 'integer', - }, - type: { - type: 'string', - }, - name: { - type: 'string', - }, - starSign: { - type: 'string', - }, - }, - required: ['id', 'type', 'name', 'starSign'], - }, - ], + $schema: 'http://json-schema.org/draft-04/schema#', + type: 'array', + items: [ + { + type: 'object', + properties: { + id: { + type: 'integer', + }, + type: { + type: 'string', + }, + name: { + type: 'string', + }, + starSign: { + type: 'string', + }, + }, + required: ['id', 'type', 'name', 'starSign'], + }, + { + type: 'object', + properties: { + id: { + type: 'integer', + }, + type: { + type: 'string', + }, + name: { + type: 'string', + }, + starSign: { + type: 'string', + }, + }, + required: ['id', 'type', 'name', 'starSign'], + }, + { + type: 'object', + properties: { + id: { + type: 'integer', + }, + type: { + type: 'string', + }, + name: { + type: 'string', + }, + starSign: { + type: 'string', + }, + }, + required: ['id', 'type', 'name', 'starSign'], + }, + { + type: 'object', + properties: { + id: { + type: 'integer', + }, + type: { + type: 'string', + }, + name: { + type: 'string', + }, + starSign: { + type: 'string', + }, + }, + required: ['id', 'type', 'name', 'starSign'], + }, + ], }; describe('Cats demo endpoint contains expected information', () => { - it('checks server is running and serving data', () => { - cy.request('/api/cats').then((response) => { - expect(response.status).to.eq(200); - expect(response.body).length.to.be.greaterThan(0); - }); - }); + it('checks server is running and serving data', () => { + cy.request('/api/cats').then((response) => { + expect(response.status).to.eq(200); + expect(response.body).length.to.be.greaterThan(0); + }); + }); - it('should validate returned JSON against schema', () => { - cy.request('GET', '/api/cats').then((response) => { - expect(response.body).to.be.jsonSchema(catsSchema); - }); - }); + it('should validate returned JSON against schema', () => { + cy.request('GET', '/api/cats').then((response) => { + expect(response.body).to.be.jsonSchema(catsSchema); + }); + }); }); describe('Bikes demo endpoint contains expected information', () => { - it('checks server is running and serving data from GET requests with no url params', () => { - cy.request('GET', '/api/bikes').then((response) => { - expect(response.status).to.eq(200); + it('checks server is running and serving data from GET requests with no url params', () => { + cy.request('GET', '/api/bikes').then((response) => { + expect(response.status).to.eq(200); - expect(response.body).to.deep.equal({ - response: - 'this is a GET test response from api/bikes for bike type: none', - }); - }); - }); + expect(response.body).to.deep.equal({ + response: + 'this is a GET test response from api/bikes for bike type: none', + }); + }); + }); - it('checks server is running and serving data from GET requests with url params', () => { - cy.request('GET', '/api/bikes?type=KawasakiNinja').then((response) => { - expect(response.status).to.eq(200); + it('checks server is running and serving data from GET requests with url params', () => { + cy.request('GET', '/api/bikes?type=KawasakiNinja').then((response) => { + expect(response.status).to.eq(200); - expect(response.body).to.deep.equal({ - response: - 'this is a GET test response from api/bikes for bike type: KawasakiNinja', - }); - }); - }); + expect(response.body).to.deep.equal({ + response: + 'this is a GET test response from api/bikes for bike type: KawasakiNinja', + }); + }); + }); - it('checks server is running and serving data from POST requests', () => { - cy.request('POST', '/api/bikes', { - id: 6, - userId: 'Mario', - title: 'Favourite Bike', - body: '{favouriteBike: "Kawasaki Ninja"}', - }).then((response) => { - expect(response.status).to.eq(200); - cy.log(response.body); + it('checks server is running and serving data from POST requests', () => { + cy.request('POST', '/api/bikes', { + id: 6, + userId: 'Mario', + title: 'Favourite Bike', + body: '{favouriteBike: "Kawasaki Ninja"}', + }).then((response) => { + expect(response.status).to.eq(200); + cy.log(response.body); - expect(response.body).to.deep.equal({ - response: - 'this is a POST test response from api/bikes with bodyData {"id":6,"userId":"Mario","title":"Favourite Bike","body":"{favouriteBike: \\"Kawasaki Ninja\\"}"}', - }); - }); - }); + expect(response.body).to.deep.equal({ + response: + 'this is a POST test response from api/bikes with bodyData {"id":6,"userId":"Mario","title":"Favourite Bike","body":"{favouriteBike: \\"Kawasaki Ninja\\"}"}', + }); + }); + }); }); describe('Images demo endpoint contains expected information', () => { - it('checks image endpoint is running', () => { - cy.visit('/api/images'); - cy.get('h4').contains( - 'Access images stored in the src/resources/images folder using the format:', - ); - cy.get('h4').contains('Example: api/images/placeholder.png'); - }); - it('checks placeholder image demo endpoint is running', () => { - cy.request('/api/images/placeholder.png').then((response) => { - expect(response.status).to.eq(200); - }); - }); - it('checks placeholder image can be resized', () => { - cy.request('/api/images/placeholder.png?height=200&width=300').then( - (response) => { - expect(response.status).to.eq(200); - }, - ); - }); + it('checks image endpoint is running', () => { + cy.visit('/api/images'); + cy.get('h4').contains( + 'Access images stored in the src/resources/images folder using the format:', + ); + cy.get('h4').contains( + 'Available image files in src/resources/images folder:', + ); + }); + it('checks placeholder image demo endpoint is running', () => { + cy.request('/api/images/placeholder.png').then((response) => { + expect(response.status).to.eq(200); + }); + }); + it('checks placeholder image can be resized', () => { + cy.request('/api/images/placeholder.png?height=200&width=300').then( + (response) => { + expect(response.status).to.eq(200); + }, + ); + }); + it(`checks images list can be accessed`, () => { + const jsonSchemaImages = { + type: 'object', + properties: { + mediaType: { + type: 'string', + }, + files: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['mediaType', 'files'], + }; + cy.request('/api/images/list').then((response) => { + expect(response.status).to.eq(200); + expect(response.body).to.be.jsonSchema(jsonSchemaImages); + }); + }); }); describe('Videos demo endpoint contains expected information', () => { - it('checks image endpoint is running', () => { - cy.visit('/api/videos'); - cy.get('h4').contains( - 'Access videos stored in the src/resources/videos folder using the format:', - ); - cy.get('h4').contains('Example: api/videos/placeholder.mp4'); - }); - it('checks placeholder image demo endpoint is running', () => { - cy.request('/api/videos/placeholder.mp4').then((response) => { - expect(response.status).to.eq(200); - }); - }); + it('checks image endpoint is running', () => { + cy.visit('/api/videos'); + cy.get('h4').contains( + 'Access videos stored in the src/resources/videos folder using the format:', + ); + cy.get('h4').contains( + 'Available video files in src/resources/videos folder:', + ); + }); + it('checks placeholder image demo endpoint is running', () => { + cy.request('/api/videos/placeholder.mp4').then((response) => { + expect(response.status).to.eq(200); + }); + }); + it(`checks videos list can be accessed`, () => { + const jsonSchemaVideos = { + type: 'object', + properties: { + mediaType: { + type: 'string', + }, + files: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['mediaType', 'files'], + }; + cy.request('/api/videos/list').then((response) => { + expect(response.status).to.eq(200); + expect(response.body).to.be.jsonSchema(jsonSchemaVideos); + }); + }); }); describe('Markdown demo endpoint contains expected information', () => { - it('checks markdown endpoint is running', () => { - cy.visit('/api/markdown'); - cy.get('h4').contains( - 'Access markdown files stored in the src/resources/markdown folder using the format:', - ); - cy.get('h4').contains('Example: api/markdown/demo'); - }); - it('checks placeholder image demo endpoint is running', () => { - cy.visit('/api/markdown/demo'); - cy.get('h1').contains('This is a test markdown file'); - cy.get('h2').contains('Add files into the src/markdown directory'); - }); + it('checks markdown endpoint is running', () => { + cy.visit('/api/markdown'); + cy.get('h4').contains( + 'Access markdown files stored in the src/resources/markdown folder using the format:', + ); + cy.get('h4').contains('Example: api/markdown/demo'); + }); + it('checks placeholder image demo endpoint is running', () => { + cy.visit('/api/markdown/demo'); + cy.get('h1').contains('This is a test markdown file'); + cy.get('h2').contains('Add files into the src/markdown directory'); + }); }); diff --git a/images/mcp-4.png b/images/mcp-4.png new file mode 100644 index 0000000..8dd4b69 Binary files /dev/null and b/images/mcp-4.png differ diff --git a/package-lock.json b/package-lock.json index 4d9d878..6e0307b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "dependencies": { "@faker-js/faker": "^9.8.0", - "@modelcontextprotocol/sdk": "^1.12.3", + "@modelcontextprotocol/sdk": "^1.13.1", "@mswjs/data": "^0.16.2", "@mswjs/http-middleware": "^0.10.3", "@types/chai-json-schema": "^1.4.10", @@ -29,12 +29,12 @@ "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", "@types/aws-lambda": "^8.10.150", - "@types/node": "^24.0.3", - "cypress": "^14.4.1", + "@types/node": "^24.0.4", + "cypress": "^14.5.0", "husky": "^9.1.7", "lint-staged": "^16.1.2", - "oxlint": "^1.1.0", - "prettier": "3.5.3", + "oxlint": "^1.3.0", + "prettier": "3.6.1", "start-server-and-test": "^2.0.12" } }, @@ -1579,9 +1579,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz", - "integrity": "sha512-DyVYSOafBvk3/j1Oka4z5BWT8o4AFmoNyZY9pALOm7Lh3GZglR71Co4r4dEUoqDWdDazIZQHBe7J2Nwkg6gHgQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.1.tgz", + "integrity": "sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -2025,9 +2025,9 @@ "license": "MIT" }, "node_modules/@oxlint/darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.1.0.tgz", - "integrity": "sha512-sSnR3SOxIU/QfaqXrcQ0UVUkzJO0bcInQ7dMhHa102gVAgWjp1fBeMVCM0adEY0UNmEXrRkgD/rQtQgn9YAU+w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-1.3.0.tgz", + "integrity": "sha512-TcCaETXYfiEfS+u/gZNND4WwEEtnJJjqg8BIC56WiCQDduYTvmmbQ0vxtqdNXlFzlvmRpZCSs7qaqXNy8/8FLA==", "cpu": [ "arm64" ], @@ -2039,9 +2039,9 @@ ] }, "node_modules/@oxlint/darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.1.0.tgz", - "integrity": "sha512-Jvd3fHnzY2OYbmsg9NSGPoBkGViDGHSFnBKyJQ9LOIw7lxAyQBG2Quxc3GYPFR/f9OYho9C3p4+dIaAJfKhnsw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/darwin-x64/-/darwin-x64-1.3.0.tgz", + "integrity": "sha512-REgq9s1ZWuh++Vi+mUPNddLTp/D+iu+T8nLd3QM1dzQoBD/SZ7wRX3Mdv8QGT/m8dknmDBQuKAP6T47ox9HRSA==", "cpu": [ "x64" ], @@ -2053,9 +2053,9 @@ ] }, "node_modules/@oxlint/linux-arm64-gnu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.1.0.tgz", - "integrity": "sha512-MgW4iskOdXuoR+wDXIJUfbdnTg2eo2FnQRaD6ZqhnDTDa7LnV+06rp/Cg3aGj2X9jSEcKDv/bMbYQuot7WRs6Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-gnu/-/linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-QAS8AWKDcDeUe8mJaw/pF2D9+js8FbFTo75AiekZKNm9V6QAAiCkyvesmILD8RrStw9aV2D/apOD71vsfcDoGA==", "cpu": [ "arm64" ], @@ -2067,9 +2067,9 @@ ] }, "node_modules/@oxlint/linux-arm64-musl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.1.0.tgz", - "integrity": "sha512-a+pkEKmDRdrW+y0gtZ/m68ElVW2VZgATGbMxDgDYFpdiMx9Y0pUPwTMZ2EX/17Aslop4c1BiDSFDK7aEBxKR2g==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-arm64-musl/-/linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-rAbz0KFkk5GPdERoFO4ZUZmVkECnHXjRG0O2MeT5zY7ddlyZUjEk1cWjw+HCtWVdKkqhZJeNFMuEiRLkpzBIIw==", "cpu": [ "arm64" ], @@ -2081,9 +2081,9 @@ ] }, "node_modules/@oxlint/linux-x64-gnu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.1.0.tgz", - "integrity": "sha512-wNBsXCKVZMvUTcFitrV1wTsdhUAv8l+XQxHxciZ2SO6dpNnWEb2YCxSAIOXeyzBLdO4pIODYcSy38CvGue7TwA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-gnu/-/linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-6uLO1WsJwCtVNGHtjXwg2TRvxQYttYJKMjSdv6RUXGWY1AI+/+yHzvu+phU/F40uNC7CFhFnqWDuPaSZ49hdAQ==", "cpu": [ "x64" ], @@ -2095,9 +2095,9 @@ ] }, "node_modules/@oxlint/linux-x64-musl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.1.0.tgz", - "integrity": "sha512-pZD0lt6A5j2Wp70fgIYk4GoPfKTZ8mHWamWIpKFT7aSkFkiOi6nhLWDFvMEIHWRTK3LgkWUNcnWPp4brvin4wQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/linux-x64-musl/-/linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-+vrmJUHgtJmgIo+L9eTP04NI/OQNCOZtQo6I49qGWc9cpr+0MnIh9KMcyAOxmzVTF5g+CF1I/1bUz4pk4I3LDw==", "cpu": [ "x64" ], @@ -2109,9 +2109,9 @@ ] }, "node_modules/@oxlint/win32-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.1.0.tgz", - "integrity": "sha512-rT6uXQvE80+B+L04HJf30uF26426FPI9i9DAY2AxBUhrpNwhqkDEhQdd9ilFWVC7SSbpHgAs50lo+ImSAAkHPQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-arm64/-/win32-arm64-1.3.0.tgz", + "integrity": "sha512-k+ETUVl+O3b8Rcd2PP5V3LqQ2QoN/TOX2f19XXHZEynbVLY3twLYPb3hLdXqoo7CKRq3RJdTfn1upHH48/qrZQ==", "cpu": [ "arm64" ], @@ -2123,9 +2123,9 @@ ] }, "node_modules/@oxlint/win32-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.1.0.tgz", - "integrity": "sha512-x6r5yvM3wEty93Bx0NuNK+kutUyS/K55itkUrxdExoK6GcmVDboGGuhju9HyU2cM/IWLEWO8RHcXSyaxr9GR5g==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxlint/win32-x64/-/win32-x64-1.3.0.tgz", + "integrity": "sha512-nWSgK0fT02TQ/BiAUCd13BaobtHySkCDcQaL+NOmhgeb0tNWjtYiktuluahaIqFcYJPWczVlbs8DU/Eqo8vsug==", "cpu": [ "x64" ], @@ -2297,9 +2297,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", "license": "MIT", "dependencies": { "undici-types": "~7.8.0" @@ -3349,9 +3349,9 @@ } }, "node_modules/cypress": { - "version": "14.4.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.4.1.tgz", - "integrity": "sha512-YSGvVXtTqSGRTyHbaxHI5dHU/9xc5ymaTIM4BU85GKhj980y6XgA3fShSpj5DatS8knXMsAvYItQxVQFHGpUtw==", + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.0.tgz", + "integrity": "sha512-1HOnKvWep0LkWuFwPeWkZ0TDl7ivi2/Mz+WNU4dfkeLJaFndS3Ow6TXT7YjuTqLFI2peJKzPKljVUFdymI2K5g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3382,6 +3382,7 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", + "hasha": "5.2.2", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -4778,6 +4779,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6001,9 +6029,9 @@ "license": "MIT" }, "node_modules/oxlint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.1.0.tgz", - "integrity": "sha512-OVNpaoaQCUHHhCv5sYMPJ7Ts5k7ziw0QteH1gBSwF3elf/8GAew2Uh/0S7HsU1iGtjhlFy80+A8nwIb3Tq6m1w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.3.0.tgz", + "integrity": "sha512-PzAOmPxnXYpVF1q6h9pkOPH6uJ/44XrtFWJ8JcEMpoEq9HISNelD3lXhACtOAW8CArjLy/qSlu2KkyPxnXgctA==", "dev": true, "license": "MIT", "bin": { @@ -6017,14 +6045,14 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/darwin-arm64": "1.1.0", - "@oxlint/darwin-x64": "1.1.0", - "@oxlint/linux-arm64-gnu": "1.1.0", - "@oxlint/linux-arm64-musl": "1.1.0", - "@oxlint/linux-x64-gnu": "1.1.0", - "@oxlint/linux-x64-musl": "1.1.0", - "@oxlint/win32-arm64": "1.1.0", - "@oxlint/win32-x64": "1.1.0" + "@oxlint/darwin-arm64": "1.3.0", + "@oxlint/darwin-x64": "1.3.0", + "@oxlint/linux-arm64-gnu": "1.3.0", + "@oxlint/linux-arm64-musl": "1.3.0", + "@oxlint/linux-x64-gnu": "1.3.0", + "@oxlint/linux-x64-musl": "1.3.0", + "@oxlint/win32-arm64": "1.3.0", + "@oxlint/win32-x64": "1.3.0" } }, "node_modules/p-map": { @@ -6190,9 +6218,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", + "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 806ae4d..a155e86 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@faker-js/faker": "^9.8.0", - "@modelcontextprotocol/sdk": "^1.12.3", + "@modelcontextprotocol/sdk": "^1.13.1", "@mswjs/data": "^0.16.2", "@mswjs/http-middleware": "^0.10.3", "@types/chai-json-schema": "^1.4.10", @@ -44,12 +44,12 @@ "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", "@types/aws-lambda": "^8.10.150", - "@types/node": "^24.0.3", - "cypress": "^14.4.1", + "@types/node": "^24.0.4", + "cypress": "^14.5.0", "husky": "^9.1.7", "lint-staged": "^16.1.2", - "oxlint": "^1.1.0", - "prettier": "3.5.3", + "oxlint": "^1.3.0", + "prettier": "3.6.1", "start-server-and-test": "^2.0.12" }, "lint-staged": { diff --git a/src/api/images/api.ts b/src/api/images/api.ts index 37313a1..4a2095c 100644 --- a/src/api/images/api.ts +++ b/src/api/images/api.ts @@ -8,11 +8,16 @@ import sharp from 'sharp'; function handler(pathName: string) { return [ http.get(`/${pathName}`, () => { + const images = fs.readdirSync('./src/resources/images'); return HttpResponse.text( `

Access images stored in the src/resources/images folder using the format: api/images/{filename}

-

Example: api/images/placeholder.png

+

Resize images by adding url paramters E.g placeholder.png?height=500&width=500

+ +

Get a full list of images as a json object at /images/list

+

Available image files in src/resources/images folder:

+
${images.map((image) => `${image}

`).join('')}
`, @@ -24,6 +29,15 @@ function handler(pathName: string) { }, ); }), + http.get(`/${pathName}/list`, () => { + return HttpResponse.json( + { + mediaType: 'image', + files: fs.readdirSync('./src/resources/images'), + }, + {}, + ); + }), http.get(`/${pathName}/:imageID`, async ({ request }) => { const url = new URL(request.url); const width = url.searchParams.get('width'); diff --git a/src/api/videos/api.ts b/src/api/videos/api.ts index a70966f..b6e2a1d 100644 --- a/src/api/videos/api.ts +++ b/src/api/videos/api.ts @@ -7,11 +7,15 @@ import { http, HttpResponse } from 'msw'; function handler(pathName: string) { return [ http.get(`/${pathName}`, ({ request: _request }) => { + const videos = fs.readdirSync('./src/resources/videos'); return HttpResponse.text( `

Access videos stored in the src/resources/videos folder using the format: api/videos/{filename}

Example: api/videos/placeholder.mp4

+

Get a full list of videos as a json object at /videos/list

+

Available video files in src/resources/videos folder:

+
${videos.map((video) => `${video}

`).join('')}
`, @@ -23,6 +27,15 @@ function handler(pathName: string) { }, ); }), + http.get(`/${pathName}/list`, () => { + return HttpResponse.json( + { + mediaType: 'video', + files: fs.readdirSync('./src/resources/videos'), + }, + {}, + ); + }), http.get(`/${pathName}/:videoID`, async ({ request }) => { const url = new URL(request.url); const params = url.pathname.split('/').pop(); diff --git a/src/mcp/helpers/add-media-endpoint.js b/src/mcp/helpers/add-media-endpoint.js new file mode 100644 index 0000000..de9ce8c --- /dev/null +++ b/src/mcp/helpers/add-media-endpoint.js @@ -0,0 +1,96 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import sharp from 'sharp'; +import https from 'node:https'; +import http from 'node:http'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +// Helper function to download file from URL +const downloadFile = (url) => { + return new Promise((resolve, reject) => { + const client = url.startsWith('https:') ? https : http; + client + .get(url, (response) => { + if (response.statusCode !== 200) { + reject( + new Error(`Failed to download: ${response.statusCode}`), + ); + return; + } + const chunks = []; + response.on('data', (chunk) => chunks.push(chunk)); + response.on('end', () => resolve(Buffer.concat(chunks))); + response.on('error', reject); + }) + .on('error', reject); + }); +}; +// Function to add media to a specified directory in a local file system +const addMediaEndpoint = async (mediaName, type, fileType, image) => { + if (!mediaName || !type || !fileType || !image) { + return 'Media API Endpoint Not created. mediaName, type, fileType, and image are required to create a new API endpoint.'; + } + // Create path to the endpoint directory + const endpointDir = path.join(__dirname, '..', '..', 'resources', type); + const apiPath = path.join(endpointDir, `${mediaName}.${fileType}`); + if (fs.existsSync(apiPath)) { + return `API Endpoint Not created. API endpoint ${mediaName}.${fileType} already exists.`; + } + try { + // Create the endpoint directory first + fs.mkdirSync(endpointDir, { recursive: true }); + let sourceBuffer = null; + let sourcePath = null; + // Determine input type and get buffer/path + if (image.startsWith('http://') || image.startsWith('https://')) { + // Download from URL + console.log(`Downloading from URL: ${image}`); + sourceBuffer = await downloadFile(image); + } else if (image.startsWith('data:')) { + // Handle data URL format + const base64Data = image.split(',')[1]; + sourceBuffer = Buffer.from(base64Data, 'base64'); + } else if (fs.existsSync(image)) { + // Handle local file path + sourcePath = image; + } else if (image.match(/^[A-Za-z0-9+/=]+$/)) { + // Assume it's a base64 string + sourceBuffer = Buffer.from(image, 'base64'); + } else { + throw new Error( + 'Invalid input: must be URL, file path, or base64 string', + ); + } + // Process based on file type + if (fileType === 'png' && type === 'images') { + if (sourcePath) { + // Use sharp with file path + await sharp(sourcePath) + .resize(1000, 1000) + .png() + .toFile(apiPath); + } else if (sourceBuffer) { + // Use sharp with buffer + await sharp(sourceBuffer) + .resize(1000, 1000) + .png() + .toFile(apiPath); + } + } else if (fileType === 'mp4' && type === 'videos') { + if (sourcePath) { + // Copy video file directly + fs.copyFileSync(sourcePath, apiPath); + } else if (sourceBuffer) { + // Write buffer to file + fs.writeFileSync(apiPath, sourceBuffer); + } + } else { + throw new Error(`Unsupported combination: ${fileType} for ${type}`); + } + return `API Endpoint ${mediaName} created successfully at ${apiPath}.`; + } catch (error) { + return `Failed to create API endpoint: ${error instanceof Error ? error.message : 'Unknown error'}`; + } +}; +export { addMediaEndpoint }; diff --git a/src/mcp/helpers/add-media-endpoint.ts b/src/mcp/helpers/add-media-endpoint.ts new file mode 100644 index 0000000..d80db83 --- /dev/null +++ b/src/mcp/helpers/add-media-endpoint.ts @@ -0,0 +1,114 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import sharp from 'sharp'; +import https from 'node:https'; +import http from 'node:http'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Helper function to download file from URL +const downloadFile = (url: string): Promise => { + return new Promise((resolve, reject) => { + const client = url.startsWith('https:') ? https : http; + + client + .get(url, (response) => { + if (response.statusCode !== 200) { + reject( + new Error(`Failed to download: ${response.statusCode}`), + ); + return; + } + + const chunks: Buffer[] = []; + response.on('data', (chunk) => chunks.push(chunk)); + response.on('end', () => resolve(Buffer.concat(chunks))); + response.on('error', reject); + }) + .on('error', reject); + }); +}; + +// Function to add media to a specified directory in a local file system +const addMediaEndpoint = async ( + mediaName: string, + type: 'videos' | 'images', + fileType: 'png' | 'mp4', + image: string, // This can be a base64 string, data URL, file path, or URL +) => { + if (!mediaName || !type || !fileType || !image) { + return 'Media API Endpoint Not created. mediaName, type, fileType, and image are required to create a new API endpoint.'; + } + + // Create path to the endpoint directory + const endpointDir = path.join(__dirname, '..', '..', 'resources', type); + const apiPath = path.join(endpointDir, `${mediaName}.${fileType}`); + + if (fs.existsSync(apiPath)) { + return `API Endpoint Not created. API endpoint ${mediaName}.${fileType} already exists.`; + } + + try { + // Create the endpoint directory first + fs.mkdirSync(endpointDir, { recursive: true }); + + let sourceBuffer: Buffer | null = null; + let sourcePath: string | null = null; + + // Determine input type and get buffer/path + if (image.startsWith('http://') || image.startsWith('https://')) { + // Download from URL + console.log(`Downloading from URL: ${image}`); + sourceBuffer = await downloadFile(image); + } else if (image.startsWith('data:')) { + // Handle data URL format + const base64Data = image.split(',')[1]; + sourceBuffer = Buffer.from(base64Data, 'base64'); + } else if (fs.existsSync(image)) { + // Handle local file path + sourcePath = image; + } else if (image.match(/^[A-Za-z0-9+/=]+$/)) { + // Assume it's a base64 string + sourceBuffer = Buffer.from(image, 'base64'); + } else { + throw new Error( + 'Invalid input: must be URL, file path, or base64 string', + ); + } + + // Process based on file type + if (fileType === 'png' && type === 'images') { + if (sourcePath) { + // Use sharp with file path + await sharp(sourcePath) + .resize(1000, 1000) + .png() + .toFile(apiPath); + } else if (sourceBuffer) { + // Use sharp with buffer + await sharp(sourceBuffer) + .resize(1000, 1000) + .png() + .toFile(apiPath); + } + } else if (fileType === 'mp4' && type === 'videos') { + if (sourcePath) { + // Copy video file directly + fs.copyFileSync(sourcePath, apiPath); + } else if (sourceBuffer) { + // Write buffer to file + fs.writeFileSync(apiPath, sourceBuffer); + } + } else { + throw new Error(`Unsupported combination: ${fileType} for ${type}`); + } + + return `API Endpoint ${mediaName} created successfully at ${apiPath}.`; + } catch (error) { + return `Failed to create API endpoint: ${error instanceof Error ? error.message : 'Unknown error'}`; + } +}; + +export { addMediaEndpoint }; diff --git a/src/mcp/server.js b/src/mcp/server.js index c2b5a6f..8054549 100644 --- a/src/mcp/server.js +++ b/src/mcp/server.js @@ -4,6 +4,7 @@ import { z } from 'zod'; import { getApiEndpoints } from './helpers/get-all-endpoints.js'; import { addApiEndpoint } from './helpers/add-api-endpoint.js'; import { apiHandlerExample } from './data/api-handler-example.js'; +import { addMediaEndpoint } from './helpers/add-media-endpoint.js'; import { startMockServer, stopMockServer, @@ -151,6 +152,40 @@ server.tool( } }, ); +server.tool( + 'create_new_media_endpoint', + `create new media api endpoint for the local mock API server by passing a base64 encoded string of an image or video, a path to a locally saved file or a url containing the media. Once saved to the local system this media can be then be accessed from the endpoint at + http://localhost:8000/api/{images|videos}/mediaName.fileType. + A list of ALL media files in a folder can be obtained from http://localhost:8000/api/{images|videos}/list. + Images and videos should be 1000px x 1000px. + Args: + - mediaName: file name for the media endpoint + - type: type of media (images or videos) + - fileType: type of file (png or mp4) + - image: This can be a base64 string, data URL, file path, or URL +`, + { + mediaName: z.string().min(1, 'Media FileName is required'), + type: z.enum(['images', 'videos']), + fileType: z.enum(['png', 'mp4']), + image: z.string(), + }, + async (input) => { + return { + content: [ + { + type: 'text', + text: await addMediaEndpoint( + input.mediaName, + input.type, + input.fileType, + input.image, + ), + }, + ], + }; + }, +); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 411cdd4..e265ef0 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { getApiEndpoints } from './helpers/get-all-endpoints.js'; import { addApiEndpoint } from './helpers/add-api-endpoint.js'; import { apiHandlerExample } from './data/api-handler-example.js'; +import { addMediaEndpoint } from './helpers/add-media-endpoint.js'; import { startMockServer, stopMockServer, @@ -165,6 +166,42 @@ server.tool( }, ); +server.tool( + 'create_new_media_endpoint', + `create new media api endpoint for the local mock API server by passing a base64 encoded string of an image or video, a path to a locally saved file or a url containing the media. Once saved to the local system this media can be then be accessed from the endpoint at + http://localhost:8000/api/{images|videos}/mediaName.fileType. + A list of ALL media files in a folder can be obtained from http://localhost:8000/api/{images|videos}/list. + Images and videos should be 1000px x 1000px. + If running in docker the server will need to be rebuilt to see the new media. + Args: + - mediaName: file name for the media endpoint + - type: type of media (images or videos) + - fileType: type of file (png or mp4) + - image: This can be a base64 string, data URL, file path, or URL +`, + { + mediaName: z.string().min(1, 'Media FileName is required'), + type: z.enum(['images', 'videos']), + fileType: z.enum(['png', 'mp4']), + image: z.string(), + }, + async (input) => { + return { + content: [ + { + type: 'text', + text: await addMediaEndpoint( + input.mediaName, + input.type, + input.fileType, + input.image, + ), + }, + ], + }; + }, +); + async function main() { const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/src/resources/images/placeholder2.png b/src/resources/images/placeholder2.png new file mode 100644 index 0000000..10189d6 Binary files /dev/null and b/src/resources/images/placeholder2.png differ diff --git a/src/resources/videos/placeholder2.mp4 b/src/resources/videos/placeholder2.mp4 new file mode 100644 index 0000000..c415116 Binary files /dev/null and b/src/resources/videos/placeholder2.mp4 differ