UNPKG

gridly-mcp-server

Version:

MCP server for Gridly API integration

869 lines (868 loc) 29.6 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import zodToJsonSchema from "zod-to-json-schema"; const API_BASE = "https://api.gridly.com/v1"; const API_KEY = process.env.GRIDLY_API_KEY; if (!API_KEY) { throw new Error("GRIDLY_API_KEY variable is required!!"); } const server = new Server({ name: "gridly-mcp-server", version: "0.1.0", }, { capabilities: { tools: {}, }, }); const projectIdSchema = z.object({ projectId: z.number().describe("Please provide the project ID"), }); const optionalProjectIdSchema = z.object({ projectId: z .number() .optional() .describe("Please provide the project ID (optional)"), }); const databaseIdSchema = z.object({ databaseId: z.string().describe("Please provide the database ID"), }); const gridIdSchema = z.object({ gridId: z.string().describe("Please provide the grid ID"), }); const gridMetadataSchema = z.object({ metadata: z .record(z.any()) .optional() .describe("Metadata object containing properties of the grid. Passing null into value will remove that key-value property (Optional)"), }); const columnIdSchema = z.object({ columnId: z.string().describe("Please provide the column ID"), }); const columnSchema = z.object({ id: z.string().describe("Column ID"), editable: z.boolean().describe("Enable editable for this column"), }); const viewIdSchema = z.object({ viewId: z .string() .describe("ID of the view. It can be found in the API quick start right panel of your Gridly Dashboard"), }); const recordIdSchema = z.object({ recordId: z.string().describe("Id of the record to getting history"), }); const dependencyIdSchema = z.object({ dependencyId: z.string().describe("Please provide the dependency ID"), }); const viewColumnSchema = viewIdSchema.merge(columnIdSchema); const viewDependencySchema = viewIdSchema.merge(dependencyIdSchema); const createGridSchema = z.object({ ...databaseIdSchema.shape, name: z.string().describe("Please provide the grid name"), templateGridId: z .string() .optional() .describe("Please provide the template grid ID (Optional)"), ...gridMetadataSchema.shape, }); const updateGridSchema = z.object({ ...gridIdSchema.shape, name: z.string().describe("Please provide the grid name"), ...gridMetadataSchema.shape, }); const createViewSchema = z.object({ name: z.string().describe("View name"), ...gridIdSchema.shape, columns: z .array(columnSchema) .optional() .describe("List of columns (Optional)"), }); const typeEnum = z.enum([ "singleLine", "multipleLines", "richText", "markdown", "singleSelection", "multipleSelections", "boolean", "number", "datetime", "files", "reference", "language", "formula", "json", "yaml", ]); const createColumnSchema = z.object({ ...viewIdSchema.shape, id: z.string().optional().describe("Column ID (Optional)"), name: z.string().describe("Please provide the column name"), type: typeEnum.describe("Please provide the column type"), }); const cellSchema = z.object({ columnId: z.string().describe("ID of a column in a view"), value: z.string().describe("Value of a cell"), }); const recordSchema = z.object({ id: z .string() .optional() .describe("This parameter specify record Id of this record"), path: z .string() .optional() .describe("This parameter specify path (folder) of this record. Use character / to indicate folder level (e.g Path Level 1/Path Level 2)"), cells: z.array(cellSchema), }); const addRecordsSchema = z.object({ ...viewIdSchema.shape, records: z.array(recordSchema), }); const deleteRecordsSchema = z.object({ ...viewIdSchema.shape, ids: z.array(z.string()).describe("List of record IDs need to be deleted"), }); const pageSchema = z.object({ offset: z.number(), limit: z.number(), }); const sortSchema = z.record(z.string().describe("The ID of the column to be sorted"), z.enum(["asc", "desc"])); const allValue = z.any(); const numberValue = z.number(); const dateValue = z.string(); const stringValue = z.string(); const arrayValue = z.array(z.any()); const betweenValue = z.tuple([z.any(), z.any()]); const regexpValue = z.instanceof(RegExp).or(z.string()); const emptyCheck = z.literal(true); const operatorUnionSchema = z .object({ "=": allValue, }) .or(z.object({ "!=": allValue, })) .or(z.object({ "<": z.union([numberValue, dateValue]), })) .or(z.object({ "<=": z.union([numberValue, dateValue]), })) .or(z.object({ ">": z.union([numberValue, dateValue]), })) .or(z.object({ ">=": z.union([numberValue, dateValue]), })) .or(z.object({ contains: stringValue, })) .or(z.object({ startsWith: stringValue, })) .or(z.object({ endsWith: stringValue, })) .or(z.object({ regexp: regexpValue, })) .or(z.object({ notRegexp: regexpValue, })) .or(z.object({ in: arrayValue, })) .or(z.object({ notIn: arrayValue, })) .or(z.object({ between: betweenValue, })) .or(z.object({ isEmpty: emptyCheck, })) .or(z.object({ isNotEmpty: emptyCheck, })); const filterSchema = z.record(z.string(), operatorUnionSchema); const listRecordsSchema = z.object({ ...viewIdSchema.shape, columnIds: z .array(z.string()) .optional() .describe("Specify data of what columns of view to include in response"), page: pageSchema.optional().describe("Starting index and number of records"), query: filterSchema.optional().describe("Criteria to filter records"), sort: sortSchema.optional().describe("Order of records"), }); const getRecordHistorySchema = z.object({ ...viewIdSchema.shape, ...recordIdSchema.shape, page: pageSchema.optional(), }); async function getProjects() { const response = await fetch(`${API_BASE}/projects`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function getProject(id) { const response = await fetch(`${API_BASE}/projects/${id}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function getDatabases(projectId) { const url = new URL(`${API_BASE}/databases`); if (projectId !== undefined) { url.searchParams.append("projectId", projectId.toString()); } const response = await fetch(url.toString(), { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function getDatabase(id) { const response = await fetch(`${API_BASE}/databases/${id}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function getGrid(id) { const response = await fetch(`${API_BASE}/grids/${id}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function createGrid(args) { const { databaseId, name } = createGridSchema.parse(args); const response = await fetch(`${API_BASE}/grids?dbId=${databaseId}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, body: JSON.stringify({ name: name, }), }); return response.json(); } async function updateGrid(args) { const { gridId, name, metadata } = updateGridSchema.parse(args); const response = await fetch(`${API_BASE}/grids/${gridId}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, body: JSON.stringify({ name: name, metadata: metadata, }), }); return response.json(); } async function deleteGrid(gridId) { const response = await fetch(`${API_BASE}/grids/${gridId}`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); if (response.status === 204) { return true; } } async function getView(id) { const response = await fetch(`${API_BASE}/views/${id}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function createView(args) { const { name, gridId, columns } = createViewSchema.parse(args); const response = await fetch(`${API_BASE}/views`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, body: JSON.stringify({ name: name, gridId: gridId, columns: columns, }), }); return response.json(); } async function getColumn(args) { const { viewId, columnId } = viewColumnSchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/columns/${columnId}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function createColumn(args) { const { viewId, name, type } = createColumnSchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/columns`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, body: JSON.stringify({ name: name, type: type, }), }); return response.json(); } async function deleteColumn(args) { const { viewId, columnId } = viewColumnSchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/columns/${columnId}`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); if (response.status === 204) { return true; } } async function addColumnToView(args) { const { viewId, columnId } = viewColumnSchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/columns/${columnId}/add`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function removeColumnFromView(args) { const { viewId, columnId } = viewColumnSchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/columns/${columnId}/remove`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); if (response.status === 200) { return true; } } async function getDependencies(viewId) { const response = await fetch(`${API_BASE}/views/${viewId}/dependencies`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function getDependency(args) { const { viewId, dependencyId } = viewDependencySchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/dependencies/${dependencyId}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function deleteDependency(args) { const { viewId, dependencyId } = viewDependencySchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/dependencies/${dependencyId}`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); if (response.status === 204) { return true; } } async function getRecords(args) { const { viewId, columnIds, page, query, sort } = listRecordsSchema.parse(args); const queryParams = []; if (columnIds) { queryParams.push(`columnIds=${columnIds}`); } if (page) { queryParams.push(`page=${encodeURIComponent(JSON.stringify(page))}`); } if (query) { queryParams.push(`query=${encodeURIComponent(JSON.stringify(query))}`); } if (sort) { queryParams.push(`sort=${encodeURIComponent(JSON.stringify(sort))}`); } const queryString = queryParams.length > 0 ? `?${queryParams.join("&")}` : ""; const response = await fetch(`${API_BASE}/views/${viewId}/records${queryString}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } async function addRecords(args) { const { viewId, records } = addRecordsSchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/records`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, body: JSON.stringify(records), }); return response.json(); } async function deleteRecords(args) { const { viewId, ids } = deleteRecordsSchema.parse(args); const response = await fetch(`${API_BASE}/views/${viewId}/records`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, body: JSON.stringify({ ids: ids, }), }); if (response.status === 204) { return true; } } async function getRecordHistory(args) { const { viewId, recordId, page } = getRecordHistorySchema.parse(args); const queryParam = page ? `?page=${encodeURIComponent(JSON.stringify(page))}` : ""; const response = await fetch(`${API_BASE}/views/${viewId}/records/${recordId}/histories${queryParam}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${API_KEY}`, }, }); return response.json(); } server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_projects", description: "List projects of a company", inputSchema: { type: "object", properties: {} }, }, { name: "retrieve_project", description: "Retrieve a project", inputSchema: zodToJsonSchema(projectIdSchema), }, { name: "list_databases", description: "List databases in a project", inputSchema: zodToJsonSchema(optionalProjectIdSchema), }, { name: "retrieve_database", description: "Retrieve a database", inputSchema: zodToJsonSchema(databaseIdSchema), }, { name: "retrieve_grid", description: "Retrieve a grid", inputSchema: zodToJsonSchema(gridIdSchema), }, { name: "create_grid", description: "Create a grid", inputSchema: zodToJsonSchema(createGridSchema), }, { name: "update_grid", description: "Update a grid", inputSchema: zodToJsonSchema(updateGridSchema), }, { name: "delete_grid", description: "Delete a grid", inputSchema: zodToJsonSchema(gridIdSchema), }, { name: "retrieve_view", description: "Retrieve a view", inputSchema: zodToJsonSchema(viewIdSchema), }, { name: "create_view", description: "Create a view", inputSchema: zodToJsonSchema(createViewSchema), }, { name: "retrieve_column", description: "Retrieve a column", inputSchema: zodToJsonSchema(viewColumnSchema), }, { name: "create_column", description: "Create a column", inputSchema: zodToJsonSchema(createColumnSchema), }, { name: "delete_column", description: "Delete a column", inputSchema: zodToJsonSchema(viewColumnSchema), }, { name: "add_column_to_view", description: "Add an existing grid column to a view", inputSchema: zodToJsonSchema(viewColumnSchema), }, { name: "remove_column_from_view", description: "Remove a column from a view", inputSchema: zodToJsonSchema(viewColumnSchema), }, { name: "list_dependencies", description: "List dependencies", inputSchema: zodToJsonSchema(viewIdSchema), }, { name: "retrieve_dependency", description: "Retrieve a dependency", inputSchema: zodToJsonSchema(viewDependencySchema), }, { name: "delete_dependency", description: "Delete a dependency", inputSchema: zodToJsonSchema(viewDependencySchema), }, { name: "add_records", description: "Add new records to a view", inputSchema: zodToJsonSchema(addRecordsSchema), }, { name: "delete_records", description: "Delete existing records of a view", inputSchema: zodToJsonSchema(deleteRecordsSchema), }, { name: "list_records", description: "List records in a view", inputSchema: zodToJsonSchema(listRecordsSchema), }, { name: "get_record_history", description: "Get record histories of a record in a view", inputSchema: zodToJsonSchema(getRecordHistorySchema), }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case "list_projects": { const projects = await getProjects(); return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], }; } case "retrieve_project": { const args = projectIdSchema.parse(request.params.arguments); const project = await getProject(args.projectId); return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }], }; } case "list_databases": { const args = projectIdSchema.partial().parse(request.params.arguments); const databases = await getDatabases(args.projectId); return { content: [ { type: "text", text: JSON.stringify(databases, null, 2), }, ], }; } case "retrieve_database": { const args = databaseIdSchema.parse(request.params.arguments); const project = await getDatabase(args.databaseId); return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }], }; } case "retrieve_grid": { const args = gridIdSchema.parse(request.params.arguments); const project = await getGrid(args.gridId); return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }], }; } case "create_grid": { const args = createGridSchema.parse(request.params.arguments); const databases = await createGrid(args); return { content: [ { type: "text", text: JSON.stringify(databases, null, 2), }, ], }; } case "update_grid": { const args = updateGridSchema.parse(request.params.arguments); const databases = await updateGrid(args); return { content: [ { type: "text", text: JSON.stringify(databases, null, 2), }, ], }; } case "delete_grid": { const args = gridIdSchema.parse(request.params.arguments); const success = await deleteGrid(args.gridId); return { content: [ { type: "text", text: success ? "Grid successfully deleted." : "Failed to delete grid.", }, ], }; } case "retrieve_view": { const args = viewIdSchema.parse(request.params.arguments); const project = await getView(args.viewId); return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }], }; } case "create_view": { const args = createViewSchema.parse(request.params.arguments); const databases = await createView(args); return { content: [ { type: "text", text: JSON.stringify(databases, null, 2), }, ], }; } case "retrieve_column": { const args = viewColumnSchema.parse(request.params.arguments); const project = await getColumn(args); return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }], }; } case "create_column": { const args = createColumnSchema.parse(request.params.arguments); const databases = await createColumn(args); return { content: [ { type: "text", text: JSON.stringify(databases, null, 2), }, ], }; } case "delete_column": { const args = viewColumnSchema.parse(request.params.arguments); const success = await deleteColumn(args); return { content: [ { type: "text", text: success ? "Column successfully deleted." : "Failed to delete column.", }, ], }; } case "add_column_to_view": { const args = viewColumnSchema.parse(request.params.arguments); const column = await addColumnToView(args); return { content: [ { type: "text", text: JSON.stringify(column, null, 2), }, ], }; } case "remove_column_from_view": { const args = viewColumnSchema.parse(request.params.arguments); const success = await removeColumnFromView(args); return { content: [ { type: "text", text: success ? "Column successfully removed from view." : "Failed to remove column from view.", }, ], }; } case "list_dependencies": { const args = viewIdSchema.parse(request.params.arguments); const databases = await getDependencies(args.viewId); return { content: [ { type: "text", text: JSON.stringify(databases, null, 2), }, ], }; } case "retrieve_dependency": { const args = viewDependencySchema.parse(request.params.arguments); const project = await getDependency(args); return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }], }; } case "delete_dependency": { const args = viewDependencySchema.parse(request.params.arguments); const success = await deleteDependency(args); return { content: [ { type: "text", text: success ? "Dependency successfully deleted." : "Failed to delete dependency.", }, ], }; } case "list_records": { const args = listRecordsSchema.parse(request.params.arguments); const records = await getRecords(args); return { content: [ { type: "text", text: JSON.stringify(records, null, 2), }, ], }; } case "add_records": { const args = addRecordsSchema.parse(request.params.arguments); const records = await addRecords(args); return { content: [ { type: "text", text: JSON.stringify(records, null, 2), }, ], }; } case "delete_records": { const args = deleteRecordsSchema.parse(request.params.arguments); const success = await deleteRecords(args); return { content: [ { type: "text", text: success ? "Record(s) successfully deleted." : "Failed to delete record(s).", }, ], }; } case "get_record_history": { const args = getRecordHistorySchema.parse(request.params.arguments); const recordHistory = await getRecordHistory(args); return { content: [ { type: "text", text: JSON.stringify(recordHistory, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${request.params.name}`); } } catch (error) { if (error instanceof z.ZodError) { throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`); } throw error; } }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Gridly MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });