Skip to content
Closed
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
7 changes: 5 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"description": "MongoDB Model Context Protocol Server",
"version": "0.0.0",
"main": "dist/index.js",
"type": "module",
"author": "MongoDB <[email protected]>",
"homepage": "https://github.com/mongodb-js/mongodb-mcp-server",
"repository": {
Expand Down
32 changes: 32 additions & 0 deletions src/common/atlas/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ApiClient } from "../../client.js";
import { State } from "../../state.js";

export async function ensureAuthenticated(state: State, apiClient: ApiClient): Promise<void> {
if (!(await isAuthenticated(state, apiClient))) {
throw new Error("Not authenticated");
}
}

export async function isAuthenticated(state: State, apiClient: ApiClient): Promise<boolean> {
switch (state.auth.status) {
case "not_auth":
return false;
case "requested":
try {
if (!state.auth.code) {
return false;
}
await apiClient.retrieveToken(state.auth.code.device_code);
return !!state.auth.token;
} catch {
return false;
}
case "issued":
if (!state.auth.token) {
return false;
}
return await apiClient.validateToken();
default:
throw new Error("Unknown authentication status");
}
}
10 changes: 8 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import path from "path";
import fs from "fs";

const packageMetadata = fs.readFileSync(path.resolve("./package.json"), "utf8");
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const dir = path.resolve(path.join(dirname(__filename), ".."));

const packageMetadata = fs.readFileSync(path.join(dir, "package.json"), "utf8");
const packageJson = JSON.parse(packageMetadata);

export const config = {
version: packageJson.version,
apiBaseURL: process.env.API_BASE_URL || "https://cloud.mongodb.com/",
clientID: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297",
stateFile: process.env.STATE_FILE || path.resolve("./state.json"),
stateFile: process.env.STATE_FILE || path.join(dir, "state.json"),
projectID: process.env.PROJECT_ID,
userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
};
Expand Down
33 changes: 33 additions & 0 deletions src/resources/atlas/clusters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";;
import { ResourceTemplateBase } from "../base.js";
import { ensureAuthenticated } from "../../common/atlas/auth.js";

export class ClustersResource extends ResourceTemplateBase {
name = "clusters";
metadata = {
description: "MongoDB Atlas clusters"
};
template = new ResourceTemplate("atlas://clusters", { list: undefined });

async execute(uri: URL, { projectId }: { projectId: string }) {
await ensureAuthenticated(this.state, this.apiClient);

const clusters = await this.apiClient.listProjectClusters(projectId);

if (!clusters || clusters.results.length === 0) {
return {
contents: [],
};
}

return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(clusters.results),
},
],
};
}
};
37 changes: 37 additions & 0 deletions src/resources/atlas/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ensureAuthenticated } from "../../common/atlas/auth.js";
import { ResourceUriBase } from "../base.js";

export class ProjectsResource extends ResourceUriBase {
name = "projects";
metadata = {
description: "MongoDB Atlas projects"
};
uri = "atlas://projects";

async execute(uri: URL) {
await ensureAuthenticated(this.state, this.apiClient);

const projects = await this.apiClient.listProjects();

if (!projects) {
return {
contents: [],
};
}

const projectList = projects.results.map((project) => ({
id: project.id,
name: project.name,
}));

return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(projectList),
},
],
};
}
};
49 changes: 49 additions & 0 deletions src/resources/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { McpServer, ResourceMetadata, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";;
import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";;
import { State } from "../state.js";
import { ApiClient } from "../client.js";
import { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";;
import { Variables } from "@modelcontextprotocol/sdk/shared/uriTemplate.js";;

abstract class ResourceCommonBase {
protected abstract name: string;
protected abstract metadata?: ResourceMetadata;

constructor(protected state: State, protected apiClient: ApiClient) { }
}

export abstract class ResourceUriBase extends ResourceCommonBase {
protected abstract uri: string;

abstract execute(uri: URL, extra: RequestHandlerExtra): ReadResourceResult | Promise<ReadResourceResult>;

register(server: McpServer) {
server.resource(
this.name,
this.uri,
this.metadata || {},
(uri: URL, extra: RequestHandlerExtra) => {
return this.execute(uri, extra);
}
);
}
}

export abstract class ResourceTemplateBase extends ResourceCommonBase {
protected abstract template: ResourceTemplate;

abstract execute(uri: URL, variables: Variables, extra: RequestHandlerExtra): ReadResourceResult | Promise<ReadResourceResult>;

register(server: McpServer) {
server.resource(
this.name,
this.template,
this.metadata || {},
(uri: URL, variables: Variables, extra: RequestHandlerExtra) => {
return this.execute(uri, variables, extra);
}
);
}
}

export type ResourceBase = ResourceTemplateBase | ResourceUriBase;
14 changes: 14 additions & 0 deletions src/resources/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ApiClient } from '../client';
import { State } from '../state';
import { ProjectsResource } from './atlas/projects.js';
import { ClustersResource } from './atlas/clusters.js';

export function registerResources(server: McpServer, state: State, apiClient: ApiClient) {
const projectsResource = new ProjectsResource(state, apiClient);
const clustersResource = new ClustersResource(state, apiClient);

projectsResource.register(server);
clustersResource.register(server);
}

13 changes: 7 additions & 6 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";;
import { ApiClient } from "./client.js";
import { State, saveState, loadState } from "./state.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { registerAtlasTools } from "./tools/atlas/tools.js";
import { registerMongoDBTools } from "./tools/mongodb/index.js";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";;
import { config } from "./config.js";
import { registerResources } from "./resources/register.js";
import { registerTools } from "./tools/register.js";

export class Server {
state: State | undefined = undefined;
Expand Down Expand Up @@ -39,9 +39,10 @@ export class Server {
version: config.version,
});

registerAtlasTools(server, this.state!, this.apiClient!);
registerMongoDBTools(server, this.state!);
registerResources(server, this.state!, this.apiClient!);
registerTools(server, this.state!, this.apiClient!);


return server;
}

Expand Down
13 changes: 0 additions & 13 deletions src/tools/atlas/atlasTool.ts

This file was deleted.

44 changes: 10 additions & 34 deletions src/tools/atlas/auth.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,21 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { ApiClient } from "../../client.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";;
import { log } from "../../logger.js";
import { saveState } from "../../state.js";
import { isAuthenticated } from "../../common/atlas/auth.js";
import { ToolBase } from "../base.js";
import { ZodRawShape } from "zod";
import { ApiClient } from "../../client.js";
import { State } from "../../state.js";
import { AtlasToolBase } from "./atlasTool.js";

export async function ensureAuthenticated(state: State, apiClient: ApiClient): Promise<void> {
if (!(await isAuthenticated(state, apiClient))) {
throw new Error("Not authenticated");
}
}

export async function isAuthenticated(state: State, apiClient: ApiClient): Promise<boolean> {
switch (state.auth.status) {
case "not_auth":
return false;
case "requested":
try {
if (!state.auth.code) {
return false;
}
await apiClient.retrieveToken(state.auth.code.device_code);
return !!state.auth.token;
} catch {
return false;
}
case "issued":
if (!state.auth.token) {
return false;
}
return await apiClient.validateToken();
default:
throw new Error("Unknown authentication status");
}
}

export class AuthTool extends AtlasToolBase {
export class AuthTool extends ToolBase<ZodRawShape> {
protected name = "auth";
protected description = "Authenticate to MongoDB Atlas";
protected argsShape = {};

constructor(state: State, private apiClient: ApiClient) {
super(state);
}

private async isAuthenticated(): Promise<boolean> {
return isAuthenticated(this.state!, this.apiClient);
}
Expand Down
Loading
Loading