diff --git a/README.md b/README.md index 38727cc..e00ccad 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ To run the server locally, use the following command: ```bash npm run serve --openapi-url="http://localhost:8081/openapi.json" +npm run serve --openapi-url="https://raw.githubusercontent.com/Roblox/creator-docs/refs/heads/main/content/en-us/reference/cloud/cloud.docs.json" --prefix="/cloud/v2" ``` Alternatively, if the package is installed globally or published to npm, you can run it directly using: diff --git a/package.json b/package.json index 401cfe0..fe1cd42 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A MCP server for used with AEP-compliant APIs.", "main": "server.ts", "type": "module", - "bin": "./src/bin.js", + "bin": "tsc && ./src/bin.js", "scripts": { "build": "tsc", "serve": "tsc && ./src/bin.js", @@ -30,4 +30,4 @@ "tsx": "^4.19.3", "winston": "^3.17.0" } -} +} \ No newline at end of file diff --git a/src/common/api/api.ts b/src/common/api/api.ts index 8c0e52b..ee1adf4 100644 --- a/src/common/api/api.ts +++ b/src/common/api/api.ts @@ -30,7 +30,7 @@ export class APIClient { "Unable to detect OAS openapi. Please add an openapi field or a openapi field" ); } - + logger.info(`reading openapi file: ${openAPI.openapi}`); const resourceBySingular: Record = {}; const customMethodsByPattern: Record = {}; diff --git a/src/logger.ts b/src/logger.ts index f34d08d..81085d5 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -10,6 +10,8 @@ export var logger = winston.createLogger({ ), transports: [ new winston.transports.Console({ + // logging to stdout will break the mcp server, so + // use stderr instead (which it a better practice anyway) stderrLevels: ["error", "warn", "info"], debugStdout: false, }), diff --git a/src/schema.ts b/src/schema.ts index 50e8cc2..7da9401 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,28 +1,24 @@ import { Resource, APISchema } from "./common/api/types.js"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; -export interface FullTool extends Tool { - resource: Resource; -} - -export function BuildCreateTool(resource: Resource, resourceName: string): FullTool { +export function BuildCreateTool(resource: Resource, resourceName: string): Tool { const schema: { type: "object"; properties: Record; required?: string[]; - } = { + } = { type: "object" as const, properties: { ...resource.schema.properties } as Record }; - + // Process pattern elements const patternElems = resource.patternElems.slice(0, -1); const required: string[] = []; - if(schema.required == undefined) { + if (schema.required == undefined) { schema.required = [] } - + for (const elem of patternElems) { if (elem.startsWith('{') && elem.endsWith('}')) { const paramName = elem.slice(1, -1); @@ -34,7 +30,7 @@ export function BuildCreateTool(resource: Resource, resourceName: string): FullT } } - if(resource.createMethod?.supportsUserSettableCreate) { + if (resource.createMethod?.supportsUserSettableCreate) { schema.required.push('id'); if (schema.properties.id) { schema.properties.id = { ...schema.properties.id, readOnly: false }; @@ -48,17 +44,16 @@ export function BuildCreateTool(resource: Resource, resourceName: string): FullT return { name: `create-${resourceName}`, description: `Create a ${resourceName}`, - inputSchema: schema, - resource: resource, + inputSchema: schema }; } -export function BuildDeleteTool(resource: Resource, resourceName: string): FullTool { +export function BuildDeleteTool(resource: Resource, resourceName: string): Tool { const schema: { type: "object"; properties: Record; required?: string[]; - } = { + } = { type: "object" as const, properties: { path: { type: "string" } @@ -70,16 +65,15 @@ export function BuildDeleteTool(resource: Resource, resourceName: string): FullT name: `delete-${resourceName}`, description: `Delete a ${resourceName}`, inputSchema: schema, - resource: resource, }; } -export function BuildUpdateTool(resource: Resource, resourceName: string): FullTool { +export function BuildUpdateTool(resource: Resource, resourceName: string): Tool { const schema: { type: "object"; properties: Record; required?: string[]; - } = { + } = { type: "object" as const, properties: { ...resource.schema.properties } as Record }; @@ -98,7 +92,6 @@ export function BuildUpdateTool(resource: Resource, resourceName: string): FullT name: `update-${resourceName}`, description: `Update a ${resourceName}`, inputSchema: schema, - resource: resource, }; } diff --git a/src/server.ts b/src/server.ts index a13fb13..f83ea7a 100755 --- a/src/server.ts +++ b/src/server.ts @@ -2,15 +2,23 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { fetchOpenAPI, OpenAPIImpl } from "./common/openapi/openapi.js"; import { APIClient } from "./common/api/api.js"; import { OpenAPI as OpenAPIType, Resource } from "./common/api/types.js"; -import { BuildCreateTool, BuildDeleteTool, BuildResource, BuildUpdateTool, FullTool } from "./schema.js"; -import { z } from "zod"; +import { BuildCreateTool, BuildDeleteTool, BuildResource, BuildUpdateTool } from "./schema.js"; import axios from "axios"; import { Client } from "./common/client/client.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + ListPromptsRequestSchema, + ReadResourceRequestSchema, + Tool +} from "@modelcontextprotocol/sdk/types.js"; import { parseArguments } from "./cli.js"; import { logger } from "./logger.js"; +import { inspect } from "util"; export async function main() { @@ -84,22 +92,29 @@ export async function main() { // Create sample tool for MCP. const resources = a.resources(); - const tools: FullTool[] = [] + const tools: Tool[] = [] const resourceList: Array<{ uriTemplate: string; name: string; mime: string }> = [] for (const [resourceName, resource] of Object.entries(resources)) { + logger.info(`Processing resource: ${resourceName}`); tools.push(BuildCreateTool(resource, resourceName)); tools.push(BuildDeleteTool(resource, resourceName)); tools.push(BuildUpdateTool(resource, resourceName)); resourceList.push(BuildResource(resource, resourceName, a.serverUrl(), prefix)) } - server.setRequestHandler(ListToolsRequestSchema, async () => { - // logger.info("Received list tools request"); + // empty handlers + server.setRequestHandler(ListResourcesRequestSchema, async () => { return { - tools: tools + resources: [] }; }); + // functional handlers + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: tools + })); + server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: resourceList })); @@ -120,7 +135,6 @@ export async function main() { }; }); - server.setRequestHandler(CallToolRequestSchema, async (request: any): Promise => { try { const toolName = request.params.name; @@ -133,9 +147,11 @@ export async function main() { throw new Error(`Matching tool not found: ${toolName}`); } if (matchingTool.name.startsWith("create")) { - const parameters = fetchParameters(request.params.arguments!, matchingTool.resource) - const body = fetchRequestBody(request.params.arguments!, matchingTool.resource) - const resp = await client.create({}, matchingTool.resource, a.serverUrl(), body, parameters) + const resourceSingular = matchingTool.name.split("-")[1]; + const resource = a.resources()[resourceSingular]; + const parameters = fetchParameters(request.params.arguments!, resource) + const body = fetchRequestBody(request.params.arguments!, resource) + const resp = await client.create({}, resource, a.serverUrl(), body, parameters) return { content: [{ type: "text",