create-knowledge-mcp 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chanthr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # create-knowledge-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Scaffolds your own [Knowledge MCP](https://github.com/chanthr/internal-mcp) server.
4
+
5
+ ```bash
6
+ npx create-knowledge-mcp my-mcp
7
+ # or: npm create knowledge-mcp@latest my-mcp
8
+ cd my-mcp
9
+ npm run dev
10
+ ```
11
+
12
+ You get `src/server.ts`, an example plugin in `src/plugins/notes.ts`, `.env`, a `Dockerfile` and a README.
13
+ Add a tool by adding a file to `src/plugins/`.
14
+
15
+ | Option | Meaning |
16
+ | --- | --- |
17
+ | `--no-install` | Skip installing packages |
18
+ | `--no-git` | Skip `git init` |
19
+
20
+ Installs with the package manager that ran it (npm, pnpm, yarn or bun). Requires Node 22.13 or later.
21
+
22
+ MIT © chanthr
package/index.js ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-knowledge-mcp — scaffolds your own Knowledge MCP server.
4
+ *
5
+ * npx create-knowledge-mcp my-mcp # or: npm create knowledge-mcp@latest my-mcp
6
+ * npx create-knowledge-mcp my-mcp --no-install --no-git
7
+ *
8
+ * No dependencies: copies ./template, fills in the project name and the framework version, installs
9
+ * packages with the package manager that ran it (npm, pnpm, yarn or bun), and prints the next steps.
10
+ * KMCP_CORE_SPEC overrides the @chnxthr/knowledge-mcp dependency (tests use a local tarball).
11
+ */
12
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
13
+ import { basename, join, resolve } from 'node:path';
14
+ import { spawnSync } from 'node:child_process';
15
+ import { createInterface } from 'node:readline/promises';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const HERE = fileURLToPath(new URL('.', import.meta.url));
19
+ const TEMPLATE = join(HERE, 'template');
20
+ const VERSION = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8')).version;
21
+ const CORE = '@chnxthr/knowledge-mcp';
22
+ // npm drops .gitignore from published packages, so the template keeps it as _gitignore.
23
+ const RENAMES = { _gitignore: '.gitignore', _dockerignore: '.dockerignore' };
24
+
25
+ const tty = process.stdout.isTTY && !process.env.NO_COLOR;
26
+ const bold = (s) => (tty ? `\x1b[1m${s}\x1b[22m` : s);
27
+ const dim = (s) => (tty ? `\x1b[2m${s}\x1b[22m` : s);
28
+ const green = (s) => (tty ? `\x1b[32m${s}\x1b[39m` : s);
29
+
30
+ function parseArgs(argv) {
31
+ const opts = { dir: undefined, install: true, git: true, help: false };
32
+ for (const a of argv) {
33
+ if (a === '--no-install') opts.install = false;
34
+ else if (a === '--no-git') opts.git = false;
35
+ else if (a === '-h' || a === '--help') opts.help = true;
36
+ else if (a.startsWith('-')) throw new Error(`unknown option ${a}`);
37
+ else if (!opts.dir) opts.dir = a;
38
+ else throw new Error(`unexpected argument ${a}`);
39
+ }
40
+ return opts;
41
+ }
42
+
43
+ /** npm package name rules, simplified: lowercase, url-safe, no leading dot or underscore. */
44
+ export function toPackageName(dir) {
45
+ return (
46
+ basename(resolve(dir))
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9._~-]+/g, '-')
49
+ .replace(/^[._-]+|[-]+$/g, '') || 'my-knowledge-mcp'
50
+ );
51
+ }
52
+
53
+ /** Which package manager started us (npm_config_user_agent = "pnpm/9.0.0 npm/? node/v22 ..."). */
54
+ export function detectPackageManager(userAgent = process.env.npm_config_user_agent ?? '') {
55
+ const name = userAgent.split(' ')[0]?.split('/')[0];
56
+ return ['pnpm', 'yarn', 'bun'].includes(name) ? name : 'npm';
57
+ }
58
+
59
+ function isEmptyDir(path) {
60
+ return !existsSync(path) || readdirSync(path).filter((f) => f !== '.git' && f !== '.DS_Store').length === 0;
61
+ }
62
+
63
+ function fill(path, vars) {
64
+ let s = readFileSync(path, 'utf8');
65
+ for (const [k, v] of Object.entries(vars)) s = s.split(`__${k}__`).join(v);
66
+ writeFileSync(path, s);
67
+ }
68
+
69
+ /** Copy the template into `target` and fill placeholders. Exported for tests. */
70
+ export function scaffold(target, { name, coreSpec }) {
71
+ mkdirSync(target, { recursive: true });
72
+ cpSync(TEMPLATE, target, { recursive: true });
73
+ for (const [from, to] of Object.entries(RENAMES)) {
74
+ if (existsSync(join(target, from))) renameSync(join(target, from), join(target, to));
75
+ }
76
+ const vars = { NAME: name, CORE_SPEC: coreSpec, VERSION };
77
+ for (const f of ['package.json', 'README.md']) fill(join(target, f), vars);
78
+ if (!existsSync(join(target, '.env'))) cpSync(join(target, '.env.example'), join(target, '.env'));
79
+ }
80
+
81
+ async function ask(question, fallback) {
82
+ if (!process.stdin.isTTY) return fallback;
83
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
84
+ try {
85
+ const a = (await rl.question(`${question} ${dim(`(${fallback})`)} `)).trim();
86
+ return a || fallback;
87
+ } finally {
88
+ rl.close();
89
+ }
90
+ }
91
+
92
+ function run(cmd, args, cwd) {
93
+ const r = spawnSync(cmd, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
94
+ return r.status === 0;
95
+ }
96
+
97
+ async function main() {
98
+ const opts = parseArgs(process.argv.slice(2));
99
+ if (opts.help) {
100
+ console.log(`Usage: npx create-knowledge-mcp [dir] [--no-install] [--no-git]`);
101
+ return;
102
+ }
103
+ const dir = opts.dir ?? (await ask('Project folder?', 'my-knowledge-mcp'));
104
+ const target = resolve(dir);
105
+ if (!isEmptyDir(target)) throw new Error(`${target} is not empty — pick a new folder`);
106
+
107
+ const name = toPackageName(dir);
108
+ const coreSpec = process.env.KMCP_CORE_SPEC || `^${VERSION}`;
109
+ scaffold(target, { name, coreSpec });
110
+ console.log(`\n${green('✓')} Created ${bold(name)} in ${target}`);
111
+
112
+ const pm = detectPackageManager();
113
+ if (opts.install) {
114
+ console.log(dim(`\n${pm} install …`));
115
+ if (!run(pm, ['install'], target)) console.log(`\n${pm} install failed — run it yourself inside ${dir}.`);
116
+ }
117
+ if (opts.git && !existsSync(join(target, '.git'))) {
118
+ spawnSync('git', ['init', '-q'], { cwd: target, stdio: 'ignore' });
119
+ }
120
+
121
+ const runScript = pm === 'npm' ? 'npm run' : pm;
122
+ const tsx = join(target, 'node_modules', '.bin', 'tsx');
123
+ console.log(`
124
+ ${bold('Next')}
125
+ cd ${dir}
126
+ ${runScript} dev ${dim('# http://localhost:8080/mcp, reloads on save')}
127
+ ${runScript} keys -- create --email you@example.com --role admin ${dim('# prints an API key once')}
128
+
129
+ ${bold('Add a tool')}
130
+ Copy src/plugins/notes.ts to src/plugins/<name>.ts and edit it. Every file there is loaded.
131
+
132
+ ${bold('Use it from Claude Code (local, no key needed)')}
133
+ claude mcp add ${name} -- ${tsx} ${join(target, 'src', 'server.ts')} serve --stdio
134
+
135
+ ${bold('Check what is on')}
136
+ ${runScript} doctor
137
+ `);
138
+ }
139
+
140
+ // Run only as a command (npx goes through a bin symlink, hence realpath), not when imported by tests.
141
+ const isMain = (() => {
142
+ try {
143
+ return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
144
+ } catch {
145
+ return false;
146
+ }
147
+ })();
148
+ if (isMain) {
149
+ main().catch((e) => {
150
+ console.error(`create-knowledge-mcp: ${e instanceof Error ? e.message : e}`);
151
+ process.exit(1);
152
+ });
153
+ }
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "create-knowledge-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "Create your own Knowledge MCP server: npx create-knowledge-mcp my-mcp",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "chanthr",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/chanthr/internal-mcp.git",
11
+ "directory": "packages/create-knowledge-mcp"
12
+ },
13
+ "homepage": "https://github.com/chanthr/internal-mcp#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/chanthr/internal-mcp/issues"
16
+ },
17
+ "keywords": ["mcp", "model-context-protocol", "create", "scaffold", "mcp-server"],
18
+ "bin": {
19
+ "create-knowledge-mcp": "index.js"
20
+ },
21
+ "files": ["index.js", "template", "README.md", "LICENSE"],
22
+ "scripts": {
23
+ "test": "node --test test/*.test.js"
24
+ },
25
+ "engines": {
26
+ "node": ">=22.13.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }
@@ -0,0 +1,35 @@
1
+ # ── Server ─────────────────────────────────────────────────────────────────
2
+ PORT=8080
3
+ # MCP_TRANSPORT=stdio # or pass `serve --stdio`
4
+ # MCP_DATA_DIR=./data # SQLite store and checkpoints (default ./data)
5
+ # MCP_STORE=supabase # use Supabase instead of the SQLite file
6
+ # SUPABASE_URL=
7
+ # SUPABASE_SERVICE_ROLE_KEY=
8
+
9
+ # ── Your plugins ───────────────────────────────────────────────────────────
10
+ # Put the env vars your plugins read here (declare them in `env: [...]` so the tools stay hidden until set).
11
+
12
+ # ── Built-in connectors (each turns on when its vars are set) ───────────────
13
+ # Google Drive: one of these, plus drive.roots in mcp.config.json
14
+ # GOOGLE_SERVICE_ACCOUNT_KEY=
15
+ # GOOGLE_OAUTH_CLIENT_ID=
16
+ # GOOGLE_OAUTH_CLIENT_SECRET=
17
+ # GOOGLE_OAUTH_REFRESH_TOKEN=
18
+ # Semantic search over Drive
19
+ # QDRANT_URL=
20
+ # QDRANT_API_KEY=
21
+ # OPENAI_API_KEY=
22
+ # Slack (bot token for reading, user token adds search)
23
+ # SLACK_BOT_TOKEN=
24
+ # SLACK_USER_TOKEN=
25
+ # GitHub (Contents: read)
26
+ # GITHUB_TOKEN=
27
+ # Railway
28
+ # RAILWAY_TOKEN=
29
+
30
+ # ── Sign-in for Claude web/desktop connectors (optional, see OAUTH_SETUP.md) ──
31
+ # MCP_OAUTH_ISSUER=https://mcp.example.com
32
+ # OAUTH_GOOGLE_CLIENT_ID=
33
+ # OAUTH_GOOGLE_CLIENT_SECRET=
34
+ # OAUTH_JWT_SECRET=
35
+ # OAUTH_ALLOWED_HD=example.com
@@ -0,0 +1,26 @@
1
+ # Your Knowledge MCP server as a container.
2
+ # docker build -t my-mcp .
3
+ # docker run -d -p 8080:8080 -v my-mcp-data:/data --env-file .env my-mcp
4
+ # docker exec <container> node dist/server.js keys create --email you@example.com --role admin
5
+ FROM node:22-alpine AS build
6
+ WORKDIR /app
7
+ COPY package*.json ./
8
+ RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
9
+ COPY tsconfig.json ./
10
+ COPY src ./src
11
+ RUN npm run build && npm prune --omit=dev
12
+
13
+ FROM node:22-alpine
14
+ WORKDIR /app
15
+ RUN addgroup -S app && adduser -S -G app -h /app app && mkdir -p /data && chown app:app /data
16
+ COPY --from=build --chown=app:app /app/node_modules ./node_modules
17
+ COPY --from=build --chown=app:app /app/dist ./dist
18
+ COPY --chown=app:app package.json ./
19
+ USER app
20
+ ENV NODE_ENV=production MCP_TRANSPORT=http PORT=8080 MCP_DATA_DIR=/data
21
+ VOLUME /data
22
+ EXPOSE 8080
23
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
24
+ CMD wget -qO- "http://127.0.0.1:${PORT}/health" >/dev/null || exit 1
25
+ ENTRYPOINT ["node", "dist/server.js"]
26
+ CMD ["serve"]
@@ -0,0 +1,75 @@
1
+ # __NAME__
2
+
3
+ Your own MCP server, built on [Knowledge MCP](https://github.com/chanthr/internal-mcp) __VERSION__.
4
+ Each file in `src/plugins/` is a plugin that adds tools. The built-in Google Drive, Slack, GitHub and Railway
5
+ plugins turn on when you set their env vars in `.env`.
6
+
7
+ ## Run
8
+
9
+ ```bash
10
+ npm run dev # http://localhost:8080/mcp, reloads on save
11
+ npm run keys -- create --email you@example.com --role admin # prints an API key once
12
+ npm run doctor # which plugins are on, and what each one is missing
13
+ ```
14
+
15
+ Clients send the key as `Authorization: Bearer <key>`. State (keys, users, plugin data) lives in `./data`.
16
+
17
+ ## Add a tool
18
+
19
+ Copy `src/plugins/notes.ts` to `src/plugins/<name>.ts` and edit it:
20
+
21
+ ```ts
22
+ import { definePlugin, defineTool, z } from '@chnxthr/knowledge-mcp';
23
+
24
+ export default definePlugin({
25
+ id: 'weather', // also the permission group
26
+ env: ['WEATHER_API_KEY'], // tools stay hidden until this is set
27
+ tools: [
28
+ defineTool({
29
+ name: 'weather_now',
30
+ description: 'Current weather for a city',
31
+ input: z.object({ city: z.string().describe('City name') }),
32
+ annotations: { readOnlyHint: true },
33
+ run: async ({ city }) => {
34
+ const r = await fetch(`https://api.example.com/weather?q=${encodeURIComponent(city)}`, {
35
+ headers: { Authorization: `Bearer ${process.env.WEATHER_API_KEY}` },
36
+ });
37
+ return r.json(); // string, any JSON value, or a full MCP result
38
+ },
39
+ }),
40
+ ],
41
+ });
42
+ ```
43
+
44
+ `run` gets the validated input and a context: `ctx.user` (email, role), `ctx.state` (persistent key-value
45
+ storage for this plugin), `ctx.config` (`plugins.<id>` from `mcp.config.json`) and `ctx.log`.
46
+ Throw `ToolError` for an expected failure. Files starting with `_` are skipped.
47
+
48
+ ## Connect a client
49
+
50
+ | Client | How |
51
+ | --- | --- |
52
+ | Claude Code (local) | `claude mcp add __NAME__ -- node /absolute/path/__NAME__/dist/server.js serve --stdio` after `npm run build` |
53
+ | Any HTTP client | URL `http://localhost:8080/mcp`, header `Authorization: Bearer <key>` |
54
+ | Claude web and desktop connectors | Deploy behind HTTPS and turn on OAuth ([guide](https://github.com/chanthr/internal-mcp/blob/main/OAUTH_SETUP.md)) |
55
+
56
+ stdio runs as the local admin unless you set `MCP_API_KEY`.
57
+
58
+ ## Permissions
59
+
60
+ ```bash
61
+ npm run users -- add teammate@example.com --role member --tools notes,drive
62
+ ```
63
+
64
+ Admins see every plugin. Members see every plugin unless `--tools` limits them. Signed-in people without
65
+ a users entry are viewers: they get `ping` and the plugins listed in `access.viewerTools` of `mcp.config.json`.
66
+
67
+ ## Deploy
68
+
69
+ ```bash
70
+ docker build -t __NAME__ .
71
+ docker run -d -p 8080:8080 -v __NAME__-data:/data --env-file .env __NAME__
72
+ docker exec <container> node dist/server.js keys create --email you@example.com --role admin
73
+ ```
74
+
75
+ Keep `/data` on a volume, or set `MCP_STORE=supabase` to keep state in Supabase.
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ dist
3
+ data
4
+ .env
5
+ .env.*
6
+ .git
7
+ *.log
8
+ mcp.config.json
9
+ *-service-account.json
@@ -0,0 +1,9 @@
1
+ node_modules/
2
+ dist/
3
+ data/
4
+ .env
5
+ .env.local
6
+ *.log
7
+ .DS_Store
8
+ mcp.config.json
9
+ *-service-account.json
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts serve",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js serve",
10
+ "keys": "tsx src/server.ts keys",
11
+ "users": "tsx src/server.ts users",
12
+ "doctor": "tsx src/server.ts doctor",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@chnxthr/knowledge-mcp": "__CORE_SPEC__"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.10.0",
20
+ "tsx": "^4.19.0",
21
+ "typescript": "^5.6.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=22.13.0"
25
+ }
26
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Example plugin — copy this file to add your own tools. Each file in src/plugins is one plugin.
3
+ *
4
+ * The plugin id ("notes") is also its permission group: admins see it, members see it unless their
5
+ * allowed tools exclude it, and viewers see it only if config access.viewerTools lists it.
6
+ */
7
+ import { definePlugin, defineTool, ToolError, z } from '@chnxthr/knowledge-mcp';
8
+
9
+ type Note = { text: string; by: string; at: string };
10
+
11
+ export default definePlugin({
12
+ id: 'notes',
13
+ description: 'Shared team notes (example plugin)',
14
+ // env: ['MY_API_KEY'], // tools stay hidden until these env vars are set
15
+ instructions: 'Notes: save short facts with notes_add, read them back with notes_list.',
16
+ tools: [
17
+ defineTool({
18
+ name: 'notes_add',
19
+ description: 'Save a short note for the team.',
20
+ input: z.object({
21
+ text: z.string().min(1).max(500).describe('The note'),
22
+ }),
23
+ run: async ({ text }, ctx) => {
24
+ const notes = (await ctx.state.get<Note[]>('all')) ?? [];
25
+ notes.push({ text, by: ctx.user.email ?? 'unknown', at: new Date().toISOString() });
26
+ await ctx.state.set('all', notes.slice(-200));
27
+ return `Saved. ${notes.length} notes in total.`;
28
+ },
29
+ }),
30
+ defineTool({
31
+ name: 'notes_list',
32
+ description: 'List the saved team notes, newest first.',
33
+ input: z.object({
34
+ limit: z.number().int().min(1).max(50).default(10).describe('How many notes'),
35
+ }),
36
+ annotations: { readOnlyHint: true },
37
+ run: async ({ limit }, ctx) => {
38
+ const notes = (await ctx.state.get<Note[]>('all')) ?? [];
39
+ if (!notes.length) throw new ToolError('No notes yet. Add one with notes_add.');
40
+ return notes.slice(-limit).reverse(); // any JSON value is returned as pretty JSON
41
+ },
42
+ }),
43
+ ],
44
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Your MCP server. Every file in ./plugins is loaded as a plugin; the built-in Drive, Slack, GitHub and
3
+ * Railway plugins turn on when their env vars are set (.env). `npm run doctor` shows what is on.
4
+ *
5
+ * The same file is the command line: serve, serve --stdio, keys, users, doctor, <plugin> <command>.
6
+ */
7
+ import { run, builtinPlugins, loadPlugins } from '@chnxthr/knowledge-mcp';
8
+
9
+ const mine = await loadPlugins(new URL('./plugins/', import.meta.url));
10
+
11
+ await run({ plugins: [...builtinPlugins, ...mine] });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src/**/*"]
16
+ }