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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -30,4 +30,4 @@
"tsx": "^4.19.3",
"winston": "^3.17.0"
}
}
}
2 changes: 1 addition & 1 deletion src/common/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Resource> = {};
const customMethodsByPattern: Record<string, CustomMethod[]> = {};

Expand Down
2 changes: 2 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down
29 changes: 11 additions & 18 deletions src/schema.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
required?: string[];
} = {
} = {
type: "object" as const,
properties: { ...resource.schema.properties } as Record<string, unknown>
};

// 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);
Expand All @@ -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 };
Expand All @@ -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<string, unknown>;
required?: string[];
} = {
} = {
type: "object" as const,
properties: {
path: { type: "string" }
Expand All @@ -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<string, unknown>;
required?: string[];
} = {
} = {
type: "object" as const,
properties: { ...resource.schema.properties } as Record<string, unknown>
};
Expand All @@ -98,7 +92,6 @@ export function BuildUpdateTool(resource: Resource, resourceName: string): FullT
name: `update-${resourceName}`,
description: `Update a ${resourceName}`,
inputSchema: schema,
resource: resource,
};
}

Expand Down
38 changes: 27 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down Expand Up @@ -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
}));
Expand All @@ -120,7 +135,6 @@ export async function main() {
};
});


server.setRequestHandler(CallToolRequestSchema, async (request: any): Promise<any> => {
try {
const toolName = request.params.name;
Expand All @@ -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",
Expand Down