@nexus-lab/create-mcp-server 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import jwt from "jsonwebtoken";
3
+
4
+ // Set environment before importing modules
5
+ const TEST_JWT_SECRET = "test-secret-for-unit-tests";
6
+ const TEST_API_KEYS = "test-key-1,test-key-2";
7
+
8
+ process.env.JWT_SECRET = TEST_JWT_SECRET;
9
+ process.env.API_KEYS = TEST_API_KEYS;
10
+
11
+ // Dynamic import to ensure env is set before module loads
12
+ const { authMiddleware, generateToken } = await import("../src/auth.js");
13
+
14
+ // Helper to create mock Express objects
15
+ function createMockReq(headers: Record<string, string> = {}) {
16
+ return {
17
+ headers,
18
+ user: undefined,
19
+ } as any;
20
+ }
21
+
22
+ function createMockRes() {
23
+ const res: any = {
24
+ statusCode: 200,
25
+ body: null,
26
+ headers: {} as Record<string, string>,
27
+ status(code: number) {
28
+ res.statusCode = code;
29
+ return res;
30
+ },
31
+ json(data: any) {
32
+ res.body = data;
33
+ return res;
34
+ },
35
+ setHeader(key: string, value: string) {
36
+ res.headers[key] = value;
37
+ return res;
38
+ },
39
+ };
40
+ return res;
41
+ }
42
+
43
+ describe("API Key Authentication", () => {
44
+ it("should authenticate with a valid API key", () => {
45
+ const req = createMockReq({ "x-api-key": "test-key-1" });
46
+ const res = createMockRes();
47
+ const next = vi.fn();
48
+
49
+ authMiddleware(req, res, next);
50
+
51
+ expect(next).toHaveBeenCalledOnce();
52
+ expect(req.user).toBeDefined();
53
+ expect(req.user.authMethod).toBe("api-key");
54
+ });
55
+
56
+ it("should reject an invalid API key", () => {
57
+ const req = createMockReq({ "x-api-key": "invalid-key" });
58
+ const res = createMockRes();
59
+ const next = vi.fn();
60
+
61
+ authMiddleware(req, res, next);
62
+
63
+ expect(next).not.toHaveBeenCalled();
64
+ expect(res.statusCode).toBe(401);
65
+ expect(res.body.error).toBe("Invalid API key");
66
+ });
67
+
68
+ it("should reject an empty API key", () => {
69
+ const req = createMockReq({ "x-api-key": "" });
70
+ const res = createMockRes();
71
+ const next = vi.fn();
72
+
73
+ authMiddleware(req, res, next);
74
+
75
+ // Empty key falls through to "no credentials" path
76
+ expect(next).not.toHaveBeenCalled();
77
+ expect(res.statusCode).toBe(401);
78
+ });
79
+ });
80
+
81
+ describe("JWT Authentication", () => {
82
+ it("should authenticate with a valid JWT", () => {
83
+ const token = jwt.sign({ sub: "user-1", role: "admin" }, TEST_JWT_SECRET, {
84
+ expiresIn: "1h",
85
+ });
86
+ const req = createMockReq({ authorization: `Bearer ${token}` });
87
+ const res = createMockRes();
88
+ const next = vi.fn();
89
+
90
+ authMiddleware(req, res, next);
91
+
92
+ expect(next).toHaveBeenCalledOnce();
93
+ expect(req.user).toBeDefined();
94
+ expect(req.user.id).toBe("user-1");
95
+ expect(req.user.role).toBe("admin");
96
+ expect(req.user.authMethod).toBe("jwt");
97
+ });
98
+
99
+ it("should reject an expired JWT", () => {
100
+ const token = jwt.sign({ sub: "user-1" }, TEST_JWT_SECRET, {
101
+ expiresIn: "-1s",
102
+ });
103
+ const req = createMockReq({ authorization: `Bearer ${token}` });
104
+ const res = createMockRes();
105
+ const next = vi.fn();
106
+
107
+ authMiddleware(req, res, next);
108
+
109
+ expect(next).not.toHaveBeenCalled();
110
+ expect(res.statusCode).toBe(401);
111
+ expect(res.body.error).toBe("Token has expired");
112
+ });
113
+
114
+ it("should reject a JWT signed with wrong secret", () => {
115
+ const token = jwt.sign({ sub: "user-1" }, "wrong-secret");
116
+ const req = createMockReq({ authorization: `Bearer ${token}` });
117
+ const res = createMockRes();
118
+ const next = vi.fn();
119
+
120
+ authMiddleware(req, res, next);
121
+
122
+ expect(next).not.toHaveBeenCalled();
123
+ expect(res.statusCode).toBe(401);
124
+ expect(res.body.error).toBe("Invalid token");
125
+ });
126
+
127
+ it("should reject an empty Bearer token", () => {
128
+ const req = createMockReq({ authorization: "Bearer " });
129
+ const res = createMockRes();
130
+ const next = vi.fn();
131
+
132
+ authMiddleware(req, res, next);
133
+
134
+ expect(next).not.toHaveBeenCalled();
135
+ expect(res.statusCode).toBe(401);
136
+ expect(res.body.error).toBe("Bearer token is empty");
137
+ });
138
+ });
139
+
140
+ describe("No Credentials", () => {
141
+ it("should return 401 with hint when no auth is provided", () => {
142
+ const req = createMockReq({});
143
+ const res = createMockRes();
144
+ const next = vi.fn();
145
+
146
+ authMiddleware(req, res, next);
147
+
148
+ expect(next).not.toHaveBeenCalled();
149
+ expect(res.statusCode).toBe(401);
150
+ expect(res.body.error).toBe("Authentication required");
151
+ expect(res.body.hint).toBeDefined();
152
+ });
153
+ });
154
+
155
+ describe("generateToken", () => {
156
+ it("should generate a valid JWT with correct claims", () => {
157
+ const token = generateToken("user-42", "admin", "1h");
158
+ const decoded = jwt.verify(token, TEST_JWT_SECRET) as jwt.JwtPayload;
159
+
160
+ expect(decoded.sub).toBe("user-42");
161
+ expect(decoded.role).toBe("admin");
162
+ expect(decoded.exp).toBeDefined();
163
+ });
164
+
165
+ it("should default to 'user' role", () => {
166
+ const token = generateToken("user-99");
167
+ const decoded = jwt.verify(token, TEST_JWT_SECRET) as jwt.JwtPayload;
168
+
169
+ expect(decoded.role).toBe("user");
170
+ });
171
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "tests"]
20
+ }
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ include: ["tests/**/*.test.ts"],
8
+ coverage: {
9
+ provider: "v8",
10
+ include: ["src/**/*.ts"],
11
+ exclude: ["src/index.ts"],
12
+ },
13
+ },
14
+ });
@@ -0,0 +1 @@
1
+ DATABASE_URL=./data.db
@@ -0,0 +1,110 @@
1
+ # MCP Server — Database Template
2
+
3
+ A production-ready [Model Context Protocol](https://modelcontextprotocol.io/) server with built-in SQLite database connectivity, powered by [Drizzle ORM](https://orm.drizzle.team/).
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ npm install
10
+
11
+ # Copy environment config
12
+ cp .env.example .env
13
+
14
+ # Build the project
15
+ npm run build
16
+
17
+ # Start the server
18
+ npm start
19
+ ```
20
+
21
+ ## Available Tools
22
+
23
+ | Tool | Description |
24
+ | --------------- | ---------------------------------------------------- |
25
+ | `create-note` | Create a new note with a title and optional content |
26
+ | `list-notes` | List all notes, with optional search query filtering |
27
+ | `get-note` | Retrieve a single note by its ID |
28
+ | `update-note` | Update a note's title and/or content by ID |
29
+ | `delete-note` | Delete a note by its ID |
30
+
31
+ ## Available Resources
32
+
33
+ | URI | Description |
34
+ | -------------- | ------------------------------------ |
35
+ | `notes://list` | All notes in the database as JSON |
36
+ | `db://schema` | Database schema documentation |
37
+
38
+ ## Configuration
39
+
40
+ ### Environment Variables
41
+
42
+ | Variable | Default | Description |
43
+ | -------------- | ------------ | --------------------------- |
44
+ | `DATABASE_URL` | `./data.db` | Path to the SQLite database |
45
+
46
+ ### Claude Desktop Integration
47
+
48
+ Add this to your Claude Desktop config (`claude_desktop_config.json`):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "my-mcp-server": {
54
+ "command": "node",
55
+ "args": ["/absolute/path/to/dist/index.js"]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Database Management
62
+
63
+ ```bash
64
+ # Generate migration files from schema changes
65
+ npm run db:generate
66
+
67
+ # Apply migrations
68
+ npm run db:migrate
69
+
70
+ # Open Drizzle Studio (visual DB browser)
71
+ npm run db:studio
72
+ ```
73
+
74
+ ## Development
75
+
76
+ ```bash
77
+ # Watch mode — recompiles on file changes
78
+ npm run dev
79
+
80
+ # Run tests
81
+ npm test
82
+ ```
83
+
84
+ ## Extending the Schema
85
+
86
+ 1. Edit `src/schema.ts` to add new tables or columns.
87
+ 2. Run `npm run db:generate` to create a migration.
88
+ 3. Run `npm run db:migrate` to apply it.
89
+ 4. Add corresponding tools in `src/tools.ts` and resources in `src/resources.ts`.
90
+
91
+ ## Project Structure
92
+
93
+ ```
94
+ ├── src/
95
+ │ ├── index.ts # Entry point — server setup and transport
96
+ │ ├── db.ts # Database connection and initialization
97
+ │ ├── schema.ts # Drizzle ORM table definitions
98
+ │ ├── tools.ts # MCP tool handlers (CRUD)
99
+ │ └── resources.ts # MCP resource handlers
100
+ ├── tests/
101
+ │ └── tools.test.ts # CRUD operation tests
102
+ ├── drizzle.config.ts # Drizzle Kit configuration
103
+ ├── vitest.config.ts # Test runner configuration
104
+ ├── tsconfig.json # TypeScript configuration
105
+ └── package.json
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .env
5
+ *.db
6
+ coverage/
7
+ drizzle/
@@ -0,0 +1,11 @@
1
+ import "dotenv/config";
2
+ import { defineConfig } from "drizzle-kit";
3
+
4
+ export default defineConfig({
5
+ schema: "./src/schema.ts",
6
+ out: "./drizzle",
7
+ dialect: "sqlite",
8
+ dbCredentials: {
9
+ url: process.env.DATABASE_URL ?? "./data.db",
10
+ },
11
+ });
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "my-mcp-server",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "MCP server with database connectivity powered by Drizzle ORM",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "start": "node dist/index.js",
11
+ "test": "vitest run",
12
+ "db:generate": "drizzle-kit generate",
13
+ "db:migrate": "drizzle-kit migrate",
14
+ "db:studio": "drizzle-kit studio"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.12.0",
18
+ "better-sqlite3": "^11.8.1",
19
+ "dotenv": "^16.5.0",
20
+ "drizzle-orm": "^0.39.3",
21
+ "zod": "^3.24.4"
22
+ },
23
+ "devDependencies": {
24
+ "@types/better-sqlite3": "^7.6.13",
25
+ "@types/node": "^22.15.3",
26
+ "drizzle-kit": "^0.31.1",
27
+ "typescript": "^5.8.3",
28
+ "vitest": "^3.1.2"
29
+ }
30
+ }
@@ -0,0 +1,51 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle, BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
3
+ import * as schema from "./schema.js";
4
+
5
+ const DATABASE_URL = process.env.DATABASE_URL ?? "./data.db";
6
+
7
+ const sqlite = new Database(DATABASE_URL);
8
+
9
+ // Enable WAL mode for better concurrent read performance
10
+ sqlite.pragma("journal_mode = WAL");
11
+ sqlite.pragma("foreign_keys = ON");
12
+
13
+ export const db: BetterSQLite3Database<typeof schema> = drizzle(sqlite, {
14
+ schema,
15
+ });
16
+
17
+ /**
18
+ * Initialize the database by creating tables if they don't exist.
19
+ * Called once at server startup.
20
+ */
21
+ export function setupDatabase(): void {
22
+ sqlite.exec(`
23
+ CREATE TABLE IF NOT EXISTS notes (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ title TEXT NOT NULL,
26
+ content TEXT NOT NULL DEFAULT '',
27
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
28
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
29
+ );
30
+ `);
31
+ }
32
+
33
+ /**
34
+ * Create a database instance from a custom SQLite connection.
35
+ * Useful for testing with in-memory databases.
36
+ */
37
+ export function createDatabase(
38
+ connection: Database.Database,
39
+ ): BetterSQLite3Database<typeof schema> {
40
+ connection.pragma("foreign_keys = ON");
41
+ connection.exec(`
42
+ CREATE TABLE IF NOT EXISTS notes (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ title TEXT NOT NULL,
45
+ content TEXT NOT NULL DEFAULT '',
46
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
47
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
48
+ );
49
+ `);
50
+ return drizzle(connection, { schema });
51
+ }
@@ -0,0 +1,30 @@
1
+ import "dotenv/config";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { setupDatabase } from "./db.js";
5
+ import { registerTools } from "./tools.js";
6
+ import { registerResources } from "./resources.js";
7
+
8
+ const server = new McpServer({
9
+ name: "my-mcp-server",
10
+ version: "0.1.0",
11
+ });
12
+
13
+ // Initialize the database (creates tables if they don't exist)
14
+ setupDatabase();
15
+
16
+ // Register all tools and resources
17
+ registerTools(server);
18
+ registerResources(server);
19
+
20
+ // Start the server with stdio transport
21
+ async function main(): Promise<void> {
22
+ const transport = new StdioServerTransport();
23
+ await server.connect(transport);
24
+ console.error("MCP server running on stdio");
25
+ }
26
+
27
+ main().catch((error: unknown) => {
28
+ console.error("Fatal error starting server:", error);
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,68 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
3
+ import { db as defaultDb } from "./db.js";
4
+ import { notes } from "./schema.js";
5
+ import type * as schema from "./schema.js";
6
+
7
+ type Db = BetterSQLite3Database<typeof schema>;
8
+
9
+ /** Database schema description exposed as a resource. */
10
+ const SCHEMA_DESCRIPTION = `# Database Schema
11
+
12
+ ## Table: notes
13
+
14
+ | Column | Type | Constraints |
15
+ |------------|---------|-------------------------------------|
16
+ | id | INTEGER | PRIMARY KEY, AUTOINCREMENT |
17
+ | title | TEXT | NOT NULL |
18
+ | content | TEXT | NOT NULL, DEFAULT '' |
19
+ | created_at | TEXT | NOT NULL, DEFAULT datetime('now') |
20
+ | updated_at | TEXT | NOT NULL, DEFAULT datetime('now') |
21
+
22
+ ### Notes
23
+ - Timestamps are stored as ISO 8601 strings (SQLite has no native datetime type).
24
+ - WAL journal mode is enabled for better concurrent read performance.
25
+ - Foreign keys are enforced.
26
+ `;
27
+
28
+ /**
29
+ * Register all resources on the given MCP server.
30
+ * Accepts an optional database override for testing.
31
+ */
32
+ export function registerResources(server: McpServer, database?: Db): void {
33
+ const db = database ?? defaultDb;
34
+
35
+ // ── notes://list ─────────────────────────────────────────────────────
36
+ server.resource("notes-list", "notes://list", {
37
+ description: "List all notes in the database as JSON",
38
+ mimeType: "application/json",
39
+ }, async () => {
40
+ const allNotes = db.select().from(notes).all();
41
+
42
+ return {
43
+ contents: [
44
+ {
45
+ uri: "notes://list",
46
+ mimeType: "application/json",
47
+ text: JSON.stringify(allNotes, null, 2),
48
+ },
49
+ ],
50
+ };
51
+ });
52
+
53
+ // ── db://schema ──────────────────────────────────────────────────────
54
+ server.resource("db-schema", "db://schema", {
55
+ description: "Database schema documentation",
56
+ mimeType: "text/markdown",
57
+ }, async () => {
58
+ return {
59
+ contents: [
60
+ {
61
+ uri: "db://schema",
62
+ mimeType: "text/markdown",
63
+ text: SCHEMA_DESCRIPTION,
64
+ },
65
+ ],
66
+ };
67
+ });
68
+ }
@@ -0,0 +1,26 @@
1
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
+ import { sql } from "drizzle-orm";
3
+
4
+ /**
5
+ * Notes table — stores user notes with title, content, and timestamps.
6
+ *
7
+ * Timestamps are stored as ISO 8601 strings (SQLite has no native date type).
8
+ * Defaults are handled at the database level via SQL expressions.
9
+ */
10
+ export const notes = sqliteTable("notes", {
11
+ id: integer("id").primaryKey({ autoIncrement: true }),
12
+ title: text("title").notNull(),
13
+ content: text("content").notNull().default(""),
14
+ createdAt: text("created_at")
15
+ .notNull()
16
+ .default(sql`(datetime('now'))`),
17
+ updatedAt: text("updated_at")
18
+ .notNull()
19
+ .default(sql`(datetime('now'))`),
20
+ });
21
+
22
+ /** TypeScript type for a note row returned from the database. */
23
+ export type Note = typeof notes.$inferSelect;
24
+
25
+ /** TypeScript type for inserting a new note. */
26
+ export type NewNote = typeof notes.$inferInsert;