MCP JSON Document Collection Server
by jimpick
- src
#!/usr/bin/env node
/**
* Maintain collections of JSON document databases with basic CRUD operations
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ServerNotificationSchema,
ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { fireproof, Database } from "@jimpick/fireproof-core";
import { connect } from "@jimpick/fireproof-cloud";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import util from "node:util";
import { parse } from "node:path";
interface JsonDocDb {
readonly name: string;
remoteName?: string;
dashboardUrl?: string;
readonly created: number;
}
interface DbInfo {
db: Database;
}
const dbs: Record<string, DbInfo> = {};
const localJsonDbCollection = fireproof("local_json_db_collection");
/*()
let cxGlobal: any = null;
const connection = await connect(db, "jim_elements_3").then((cx) => {
// console.error("Connected", cx);
cxGlobal = cx;
});
*/
// console.error(connection);
const server = new Server(
{
name: "json-db-collection",
version: "0.0.1",
},
{
capabilities: {
tools: { enabled: true },
},
}
);
// Schema definitions
const CreateDbArgsSchema = z.object({
databaseName: z.string(),
});
const DeleteDbArgsSchema = z.object({
databaseName: z.string(),
});
const ConnectDbToCloudArgsSchema = z.object({
databaseName: z.string(),
});
const SaveJsonDocToDbArgsSchema = z.object({
databaseName: z.string(),
doc: z.object({})
});
const QueryJsonDocsFromDbArgsSchema = z.object({
databaseName: z.string(),
sortField: z.string(),
});
const LoadJsonDocFromDbArgsSchema = z.object({
databaseName: z.string(),
id: z.string(),
});
const DeleteJsonDocFromDbArgsSchema = z.object({
databaseName: z.string(),
id: z.string(),
});
const ToolInputSchema = ToolSchema.shape.inputSchema;
type ToolInput = z.infer<typeof ToolInputSchema>;
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_json_doc_database",
description: "Create a JSON document database",
inputSchema: zodToJsonSchema(CreateDbArgsSchema) as ToolInput,
},
{
name: "delete_json_doc_database",
description: "Delete a JSON document database",
inputSchema: zodToJsonSchema(DeleteDbArgsSchema) as ToolInput,
required: ["databaseName"],
},
{
name: "connect_json_doc_database_to_cloud",
description: "Connect a JSON document database to cloud sync service. Show the dashboard URL after connecting.",
inputSchema: zodToJsonSchema(ConnectDbToCloudArgsSchema) as ToolInput,
required: ["databaseName"],
},
{
name: "list_json_doc_databases",
description:
"Returns the list of JSON document databases. " +
"Use this to understand which databases are available before trying to access JSON documents.",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "save_json_doc_to_db",
description: "Save a JSON document to a document database",
inputSchema: {
type: "object",
properties: {
doc: {
type: "object",
description: "JSON document to save",
},
databaseName: {
type: "string",
description: "document database to save to",
},
},
required: ["doc", "databaseName"],
},
},
{
name: "query_json_docs_from_db",
description: "Query JSON documents sorted by a field from a document database. " +
"If no sortField is provided, use the _id field.",
inputSchema: zodToJsonSchema(QueryJsonDocsFromDbArgsSchema) as ToolInput,
required: ["sortField"],
},
{
name: "load_json_doc_from_db",
description: "Load a JSON document by ID from a document database",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "ID of document to load",
},
databaseName: {
type: "string",
description: "name of document database to load from",
},
},
// properties: zodToJsonSchema(LoadJsonDocFromDbArgsSchema),
required: ["id"],
},
},
{
name: "delete_json_doc_from_db",
description: "Delete a JSON document by ID from a document database",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "ID of document to delete",
},
databaseName: {
type: "string",
description: "name of document database to delete from",
},
}
},
},
{
name: "connect_json_doc_database_to_cloud",
description: "Connect a JSON document database to cloud sync service",
inputSchema: {
type: "object",
properties: {
databaseName: {
type: "string",
description: "name of document database to connect to cloud",
},
}
},
},
/*
{
name: "dump_connection",
description: "Dump connection info",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_dashboard_url",
description: "Get dashboard URL",
inputSchema: {
type: "object",
properties: {},
},
},
*/
],
};
});
/*
case "dump_connection": {
// console.error("db", db);
// console.error("cx", cxGlobal);
return {
content: [
{
type: "text",
text: `dashboard:\n${cxGlobal.dashboardUrl}\n\ndb:\n${util.format(db)}\n\ncx:\n${util.format(cxGlobal)}`,
},
],
};
}
case "get_dashboard_url": {
return {
content: [
{
type: "text",
text: cxGlobal.dashboardUrl
},
],
};
}
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
switch (name) {
case "create_json_doc_database": {
const parsed = CreateDbArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for create_json_doc_database: ${parsed.error}`);
}
const results = await localJsonDbCollection.query<string, JsonDocDb>(
"name",
{
range: [
parsed.data.databaseName,
parsed.data.databaseName
]
});
if (results.rows.length > 0) {
throw new Error(`Database already exists: ${parsed.data.databaseName}`);
}
const newDb = fireproof(parsed.data.databaseName);
dbs[parsed.data.databaseName] = { db: newDb };
await localJsonDbCollection.put<JsonDocDb>({
name: parsed.data.databaseName,
created: Date.now(),
});
return {
content: [
{
type: "text",
text: `Created JSON document database: ${parsed.data.databaseName}`,
}
]
}
}
case "connect_json_doc_database_to_cloud": {
const parsed = ConnectDbToCloudArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for connect_json_doc_database_to_cloud: ${parsed.error}`);
}
const dbName = parsed.data.databaseName;
const results = await localJsonDbCollection.query<string, JsonDocDb>(
"name",
{
range: [
dbName,
dbName
]
});
if (results.rows.length != 1) {
throw new Error(`Database not found: ${dbName}`);
}
const doc = results.rows[0].doc!;
if (!dbs[dbName]) {
const newDb = fireproof(dbName);
dbs[dbName] = { db: newDb };
}
const db = dbs[dbName].db;
const connection = await connect(db);
const remoteName = connection.dashboardUrl?.getParam("remoteName");
// console.error("connection.dashboardUrl.remoteName", connection.dashboardUrl?.getParam("remoteName"));
const dashboardUrl = connection.dashboardUrl?.toString();
// console.error("dashboardUrl", dashboardUrl);
await localJsonDbCollection.put({...doc, remoteName, dashboardUrl});
return {
content: [
{
type: "text",
text: `Connected JSON document database to cloud: ${dbName}, dashboard URL: ${dashboardUrl}`,
}
]
}
}
case "delete_json_doc_database": {
const parsed = DeleteDbArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for delete_json_doc_database: ${parsed.error}`);
}
const results = await localJsonDbCollection.query<string, JsonDocDb>(
"name",
{
range: [
parsed.data.databaseName,
parsed.data.databaseName
]
});
if (results.rows.length != 1) {
throw new Error(`Database not found: ${parsed.data.databaseName}`);
}
await localJsonDbCollection.del(results.rows[0].id);
return {
content: [
{
type: "text",
text: `Deleted JSON document database: ${parsed.data.databaseName}`,
}
]
}
}
case "list_json_doc_databases": {
const results = await localJsonDbCollection.query<string, JsonDocDb>("name", {
includeDocs: true,
descending: true,
})
const dbNames = results.rows.flatMap(row => row.doc ? [row.doc.name] : []);
return {
content: [
{
type: "text",
text: JSON.stringify(dbNames)
}
]
}
}
case "save_json_doc_to_db": {
const parsed = SaveJsonDocToDbArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for save_json_doc_to_db: ${parsed.error}`);
}
const doc = request.params.arguments?.doc;
if (!doc) {
throw new Error("Document is required");
}
const dbName = parsed.data.databaseName;
if (!dbs[dbName]) {
const newDb = fireproof(dbName);
dbs[dbName] = { db: newDb };
}
const db = dbs[dbName].db;
const response = await db.put({
...doc,
created: Date.now(),
});
return {
content: [
{
type: "text",
text: `Saved document with ID: ${response.id} to database: ${dbName}`,
}
]
}
}
case "query_json_docs_from_db": {
const parsed = QueryJsonDocsFromDbArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for query_json_docs_from_db: ${parsed.error}`);
}
const dbName = parsed.data.databaseName;
if (!dbs[dbName]) {
const newDb = fireproof(dbName);
dbs[dbName] = { db: newDb };
}
const db = dbs[dbName].db;
const results = await db.query(parsed.data.sortField, {
includeDocs: true,
descending: true,
});
return {
content: [
{
type: "text",
text: JSON.stringify(results.rows.map((row) => row.doc)),
},
],
};
}
case "load_json_doc_from_db": {
const parsed = LoadJsonDocFromDbArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for load_json_doc_from_db: ${parsed.error}`);
}
const dbName = parsed.data.databaseName;
if (!dbs[dbName]) {
const newDb = fireproof(dbName);
dbs[dbName] = { db: newDb };
}
const db = dbs[dbName].db;
const doc = await db.get(parsed.data.id);
console.error("doc", doc);
return {
content: [
{
type: "text",
text: JSON.stringify(doc),
},
],
};
}
case "delete_json_doc_from_db": {
const parsed = DeleteJsonDocFromDbArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for delete_json_doc_from_db: ${parsed.error}`);
}
const dbName = parsed.data.databaseName;
if (!dbs[dbName]) {
const newDb = fireproof(dbName);
dbs[dbName] = { db: newDb };
}
const db = dbs[dbName].db;
await db.del(parsed.data.id);
return {
content: [
{
type: "text",
text: `Deleted document with ID: ${parsed.data.id}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
/**
* Start the server using stdio transport
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});