mikser-io-mcp 0.2.0 → 1.0.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/README.md CHANGED
@@ -15,27 +15,67 @@ MCP (Model Context Protocol) substrate and tools for [mikser-io](https://github.
15
15
  npm install mikser-io-mcp
16
16
  ```
17
17
 
18
- Peer dependencies: `mikser-io ^8.2.0`, `zod ^4.0.0`.
18
+ Peer dependencies: `mikser-io ^9.0.0`, `zod ^4.0.0`.
19
19
 
20
20
  ## Activate
21
21
 
22
- Add `'mcp'` to your mikser project's plugins array. **List it FIRST** — the plugin factory creates `runtime.options.mcp` synchronously so any plugins that register tools (api, layouts, refs, vector, etc.) can gate on it at their own `onLoaded` hook:
22
+ Import the `mcp` factory and call it **first** in your mikser project's plugins array — the closure runs synchronously and creates `runtime.options.mcp` so any plugins that register tools (api, layouts, refs, vector, etc.) can gate on it at their own `onLoaded` hook:
23
23
 
24
24
  ```js
25
25
  // mikser.config.js
26
+ import { mcp } from 'mikser-io-mcp'
27
+
26
28
  export default {
27
- plugins: ['mcp', /* … your other plugins */],
28
- mcp: {
29
- path: '/mcp', // optional; default '/mcp' (also serves as the base for `endpoints` below)
30
- endpoints: { /* … */ } // optional; same shape as the in-core era
31
- },
29
+ plugins: [
30
+ mcp({
31
+ path: '/mcp', // optional; default '/mcp' (also serves as the base for `endpoints` below)
32
+ endpoints: { /* … */ } // optional; same shape as the in-core era
33
+ }),
34
+ /* … your other plugins */
35
+ ],
32
36
  }
33
37
  ```
34
38
 
35
- If `runtime.config.mcp` is absent the plugin runs as a no-op (loads but creates no substrate, mounts no transport). That lets you list `'mcp'` in plugins without forcing config.
39
+ Calling `mcp()` with no options is a no-op activation — the factory runs but creates no substrate and mounts no transport. To skip MCP entirely, leave the factory call out of `plugins`.
36
40
 
37
41
  Run mikser with `--server`; the MCP transport mounts at the configured path on the same Express server the api / preview / data plugins use.
38
42
 
43
+ ## Register with a client
44
+
45
+ Once the plugin is installed in your mikser project, the package ships a CLI you can invoke via `npx` to connect MCP-speaking clients. Connector name + description are read automatically from the project's `package.json`.
46
+
47
+ ### Claude Desktop
48
+
49
+ ```bash
50
+ npx mikser-io-mcp register claude # default URL http://localhost:3001/mcp
51
+ npx mikser-io-mcp register claude --url http://localhost:4000/mcp # custom port
52
+ npx mikser-io-mcp register claude --dry-run # show what would change
53
+ npx mikser-io-mcp register claude --force # overwrite a different existing entry
54
+ npx mikser-io-mcp register claude --unregister # remove the entry
55
+ ```
56
+
57
+ Writes a `mcpServers` entry into Claude Desktop's per-OS config file:
58
+
59
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
60
+ - Windows: `%APPDATA%/Claude/claude_desktop_config.json`
61
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
62
+
63
+ The entry launches `supergateway` (npx-installed on demand) as a stdio→streamable-HTTP bridge to your running mikser server. Fully quit + reopen Claude Desktop after registering.
64
+
65
+ ### ChatGPT
66
+
67
+ ```bash
68
+ npx mikser-io-mcp register chatgpt --url https://YOUR-TUNNEL.example/mcp
69
+ ```
70
+
71
+ ChatGPT's MCP integration is server-side — OpenAI's servers connect to your MCP endpoint directly. `localhost` is unreachable; you must expose mikser via a public tunnel (`ngrok http 3001`, Cloudflare Tunnel, etc.) before running this.
72
+
73
+ The script doesn't write a file (ChatGPT has no local config). It prints the three fields (Name, Description, MCP Server URL) you paste into ChatGPT's UI: Settings → Connectors → Advanced → Developer mode → Create.
74
+
75
+ Notes:
76
+ - ChatGPT Developer mode + custom MCP connectors are **beta** and require a Plus / Pro / Business / Enterprise / Edu account.
77
+ - Connectors don't auto-enable per chat — toggle on each new conversation.
78
+
39
79
  ## Documentation
40
80
 
41
81
  - [Full MCP tour, twelve worked scenarios, every tool and resource](./documentation/mcp.md)
package/index.js CHANGED
@@ -655,20 +655,25 @@ function pinoLevelNumber(name) {
655
655
  // endpoints: { ... } // same shape as the previous mcp.endpoints
656
656
  // }
657
657
  //
658
- // If runtime.config.mcp is absent, the plugin runs as a no-op (loads
659
- // but creates no substrate, mounts no transport). Lets users include
660
- // 'mcp' in plugins without forcing them to also provide config.
661
- export default (core) => {
658
+ // Presence in the plugins array IS the activation signal. The `mcp`
659
+ // config block is for tuning (path, endpoints, renderTimeout). When
660
+ // absent, defaults apply. For conditional activation, toggle the
661
+ // plugins array — `...(process.env.MCP ? ['mcp'] : [])` — same shape
662
+ // other plugins use.
663
+ export function mcp(options = {}) {
664
+ return (core) => {
662
665
  // Use the runtime singleton imported above for substrate-internal
663
666
  // code paths (createMcpSubstrate etc. reference it directly via
664
667
  // closure). The factory arg `core.runtime` is the same object;
665
668
  // they're both mikser-io's exported runtime singleton.
666
669
  const { onLoaded, useLogger } = core
667
670
 
668
- if (!runtime.config.mcp) {
669
- useLogger?.()?.debug('mikser-io-mcp loaded but runtime.config.mcp is absent — no substrate created')
670
- return { name: 'mcp' }
671
- }
671
+ // Mirror plugin options into runtime.config.mcp so non-plugin
672
+ // helpers (mountMcpOnExpress and friends, called by embedders that
673
+ // wire MCP up directly without `plugins:`) keep finding their
674
+ // config there. Plugin-side reads use `options.X` and never touch
675
+ // runtime.config.mcp.
676
+ runtime.config.mcp = options
672
677
 
673
678
  // Contribute MCP transport headers to engine's CORS arrays so
674
679
  // browser-side MCP clients (basic-host, mcp-ui, etc.) can read
@@ -686,7 +691,7 @@ export default (core) => {
686
691
  // runtime.options.mcp by the time those run. The mcp plugin MUST
687
692
  // be FIRST in the user's plugins array for that contract to hold.
688
693
  runtime.options.mcp = createMcpSubstrate()
689
- runtime.options.mcpPath = runtime.config.mcp.path ?? '/mcp'
694
+ runtime.options.mcpPath = options.path ?? '/mcp'
690
695
 
691
696
  // Compose the MCP-UI surface in the same package — shell
692
697
  // resource, mikser_preview_ui, mikser_ui_action, mikser_preview_render,
@@ -916,7 +921,7 @@ export default (core) => {
916
921
  const mcp = runtime.options.mcp
917
922
 
918
923
  const { render: mcpRender } = useRenderer(runtime, {
919
- defaultTimeout: runtime.config.mcp?.renderTimeout ?? 30_000,
924
+ defaultTimeout: options.renderTimeout ?? 30_000,
920
925
  })
921
926
 
922
927
  const ok = (data) => ({
@@ -1062,4 +1067,5 @@ export default (core) => {
1062
1067
  })
1063
1068
 
1064
1069
  return { name: 'mcp' }
1070
+ }
1065
1071
  }
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "mikser-io-mcp",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "MCP (Model Context Protocol) substrate and tools for mikser-io. Extracted from core to iterate on its own release cadence.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
+ "bin": {
8
+ "mikser-io-mcp": "./scripts/index.js"
9
+ },
7
10
  "files": [
8
11
  "index.js",
9
12
  "preview.js",
10
13
  "public",
14
+ "scripts",
11
15
  "README.md",
12
16
  "LICENSE"
13
17
  ],
@@ -37,7 +41,7 @@
37
41
  "minimatch": "^10.0.3"
38
42
  },
39
43
  "peerDependencies": {
40
- "mikser-io": "^8.2.0",
44
+ "mikser-io": "^9.0.0",
41
45
  "zod": "^4.0.0"
42
46
  },
43
47
  "devDependencies": {
@@ -0,0 +1,106 @@
1
+ // register chatgpt — print copy-paste instructions for adding mikser
2
+ // to ChatGPT Desktop as a custom MCP connector.
3
+ //
4
+ // ChatGPT does NOT use a local config file. Connectors live in the
5
+ // user's OpenAI account, configured through the UI. OpenAI's servers
6
+ // connect to the MCP endpoint directly, so localhost won't work — a
7
+ // publicly-reachable HTTPS URL is mandatory.
8
+ //
9
+ // This script can't drop a file. The most it can do is render the
10
+ // exact name / description / URL the user pastes into the UI.
11
+
12
+ import { readFileSync, existsSync } from 'node:fs'
13
+ import { resolve } from 'node:path'
14
+
15
+ function parseArgs(args) {
16
+ const opts = {}
17
+ let expectUrl = false
18
+ for (const a of args) {
19
+ if (a.startsWith('--url=')) opts.url = a.slice(6)
20
+ else if (a === '--url') expectUrl = true
21
+ else if (expectUrl) { opts.url = a; expectUrl = false }
22
+ }
23
+ return opts
24
+ }
25
+
26
+ function readProjectMeta() {
27
+ const pkgPath = resolve(process.cwd(), 'package.json')
28
+ if (!existsSync(pkgPath)) {
29
+ throw new Error(`No package.json in ${process.cwd()} — run this from inside a mikser project directory.`)
30
+ }
31
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
32
+ if (!pkg.name) throw new Error('package.json has no "name" field — set one before registering.')
33
+ return {
34
+ name: pkg.name,
35
+ description: pkg.description ?? `MCP connector for ${pkg.name}.`,
36
+ }
37
+ }
38
+
39
+ export async function runChatGPT(args) {
40
+ const opts = parseArgs(args)
41
+
42
+ if (!opts.url) {
43
+ process.stderr.write(
44
+ `! ChatGPT needs a publicly-reachable HTTPS URL.
45
+
46
+ OpenAI's servers connect to your MCP endpoint directly — there's no stdio
47
+ bridge, no localhost. You'll need to expose mikser via a tunnel first:
48
+
49
+ ngrok http 3001
50
+ # or:
51
+ cloudflared tunnel --url http://localhost:3001
52
+
53
+ Then re-run with the public URL:
54
+
55
+ npx mikser-io-mcp register chatgpt --url https://YOUR-TUNNEL.example/mcp
56
+
57
+ ChatGPT's Developer mode + custom MCP connectors are currently in BETA
58
+ and require a Plus / Pro / Business / Enterprise / Edu account. See
59
+ https://help.openai.com/en/articles/12584461 for the current state.
60
+ `
61
+ )
62
+ process.exit(1)
63
+ }
64
+
65
+ const { name, description } = readProjectMeta()
66
+
67
+ process.stdout.write(
68
+ `✓ Connector details for "${name}"
69
+
70
+ ChatGPT doesn't have a local config file — connectors are added through
71
+ the UI. Copy the three fields below.
72
+
73
+ Open ChatGPT (Desktop or web) and navigate to:
74
+ Settings → Connectors → Advanced → Developer mode (toggle ON)
75
+ Click + Create
76
+
77
+ Paste these into the form:
78
+
79
+ Name: ${name}
80
+ Description: ${description}
81
+ MCP Server URL: ${opts.url}
82
+ Authentication: None (or your scheme of choice for production)
83
+
84
+ Click Create. Then to use it:
85
+
86
+ 1. Start a NEW chat (saved connectors are off-by-default per chat)
87
+ 2. Open the composer's Developer Mode tool picker
88
+ 3. Enable "${name}"
89
+ 4. Try: "Use mikser_query_entities to show me everything in this catalog"
90
+
91
+ Notes:
92
+ - Mikser must be running and the URL must be reachable from
93
+ OpenAI's servers (not just your machine).
94
+ - Connectors do NOT auto-enable per chat — toggle on for each new
95
+ conversation.
96
+ - Developer mode + arbitrary custom MCP connectors are BETA. Plus /
97
+ Pro / Business / Enterprise / Edu only; Free tier excluded.
98
+ - "ChatGPT Apps" is the GA but read-only variant of MCP integration;
99
+ this script targets the full read+write Developer mode beta.
100
+
101
+ Reference:
102
+ https://help.openai.com/en/articles/12584461
103
+ https://developers.openai.com/apps-sdk/deploy/connect-chatgpt
104
+ `
105
+ )
106
+ }
@@ -0,0 +1,122 @@
1
+ // register claude — drop a connector entry into Claude Desktop's
2
+ // config file so the app picks up the local mikser server.
3
+ //
4
+ // Claude Desktop launches MCP servers as stdio subprocesses, so we
5
+ // register supergateway as the launcher — it bridges Claude's stdio
6
+ // to mikser's streamable-HTTP endpoint. The mikser server must be
7
+ // running for Claude to find anything.
8
+
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
10
+ import { homedir, platform } from 'node:os'
11
+ import { join, dirname, resolve } from 'node:path'
12
+
13
+ const DEFAULT_URL = 'http://localhost:3001/mcp'
14
+
15
+ function configPath() {
16
+ const home = homedir()
17
+ switch (platform()) {
18
+ case 'darwin': return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
19
+ case 'win32': return join(process.env.APPDATA ?? join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
20
+ case 'linux': return join(home, '.config', 'Claude', 'claude_desktop_config.json')
21
+ default: throw new Error(`register-mcp: unsupported platform "${platform()}"`)
22
+ }
23
+ }
24
+
25
+ function parseArgs(args) {
26
+ const opts = {}
27
+ let expectUrl = false
28
+ for (const a of args) {
29
+ if (a === '--unregister') opts.unregister = true
30
+ else if (a === '--dry-run') opts.dryRun = true
31
+ else if (a === '--force') opts.force = true
32
+ else if (a.startsWith('--url=')) opts.url = a.slice(6)
33
+ else if (a === '--url') expectUrl = true
34
+ else if (expectUrl) { opts.url = a; expectUrl = false }
35
+ }
36
+ return opts
37
+ }
38
+
39
+ function readProjectName() {
40
+ const pkgPath = resolve(process.cwd(), 'package.json')
41
+ if (!existsSync(pkgPath)) {
42
+ throw new Error(`No package.json in ${process.cwd()} — run this from inside a mikser project directory.`)
43
+ }
44
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
45
+ if (!pkg.name) throw new Error('package.json has no "name" field — set one before registering.')
46
+ return pkg.name
47
+ }
48
+
49
+ export async function runClaude(args) {
50
+ const opts = parseArgs(args)
51
+ const name = readProjectName()
52
+ const url = opts.url ?? DEFAULT_URL
53
+
54
+ const entry = {
55
+ command: 'npx',
56
+ args: ['-y', 'supergateway', '--streamableHttp', url],
57
+ }
58
+
59
+ const path = configPath()
60
+ let config = {}
61
+ let existed = false
62
+
63
+ if (existsSync(path)) {
64
+ existed = true
65
+ try {
66
+ config = JSON.parse(readFileSync(path, 'utf8'))
67
+ } catch (err) {
68
+ process.stderr.write(`! Existing Claude Desktop config at\n ${path}\nis not valid JSON. Refusing to overwrite — fix it manually first.\n Error: ${err.message}\n`)
69
+ process.exit(1)
70
+ }
71
+ }
72
+
73
+ config.mcpServers = config.mcpServers || {}
74
+
75
+ // Unregister branch.
76
+ if (opts.unregister) {
77
+ if (!(name in config.mcpServers)) {
78
+ process.stdout.write(`✓ "${name}" is not registered. Nothing to do.\n`)
79
+ return
80
+ }
81
+ if (opts.dryRun) {
82
+ process.stdout.write(`[dry-run] Would remove "${name}" from ${path}\n`)
83
+ return
84
+ }
85
+ delete config.mcpServers[name]
86
+ writeFileSync(path, JSON.stringify(config, null, 2) + '\n')
87
+ process.stdout.write(`✓ Removed "${name}" from ${path}\n Restart Claude Desktop to apply.\n`)
88
+ return
89
+ }
90
+
91
+ // Idempotent — bail if already matching.
92
+ const existing = config.mcpServers[name]
93
+ if (existing && JSON.stringify(existing) === JSON.stringify(entry)) {
94
+ process.stdout.write(`✓ "${name}" is already registered with the matching config.\n Nothing to change. (Use --force to re-write anyway.)\n`)
95
+ return
96
+ }
97
+ if (existing && !opts.force) {
98
+ process.stderr.write(`! "${name}" is already registered but with a different config:\n ${JSON.stringify(existing)}\n Wanted:\n ${JSON.stringify(entry)}\n Re-run with --force to overwrite.\n`)
99
+ process.exit(1)
100
+ }
101
+ if (opts.dryRun) {
102
+ process.stdout.write(`[dry-run] Would ${existing ? 'overwrite' : 'add'} "${name}" in ${path}:\n${JSON.stringify(entry, null, 2)}\n`)
103
+ return
104
+ }
105
+
106
+ config.mcpServers[name] = entry
107
+ mkdirSync(dirname(path), { recursive: true })
108
+ writeFileSync(path, JSON.stringify(config, null, 2) + '\n')
109
+
110
+ process.stdout.write(
111
+ `✓ Registered "${name}" in
112
+ ${path}
113
+ ${existed ? '(merged into existing config)' : '(created new config file)'}
114
+
115
+ Next steps:
116
+ 1. Make sure mikser is running and reachable at ${url}
117
+ 2. Restart Claude Desktop (fully quit, then reopen)
118
+ 3. The mikser tools should appear in Claude's tool list
119
+
120
+ To remove: npx mikser-io-mcp register claude --unregister
121
+ `)
122
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // Bin entry for mikser-io-mcp. Dispatches subcommands.
3
+ //
4
+ // Usage:
5
+ // npx mikser-io-mcp register claude [--url URL] [--dry-run] [--force] [--unregister]
6
+ // npx mikser-io-mcp register chatgpt --url URL [--dry-run]
7
+ //
8
+ // Run from inside a mikser project directory — registration scripts
9
+ // read ./package.json for the project name (used as the connector
10
+ // label).
11
+
12
+ import { runClaude } from './claude.js'
13
+ import { runChatGPT } from './chatgpt.js'
14
+
15
+ function usage(code = 0) {
16
+ process.stdout.write(`mikser-io-mcp — register this project with an MCP-speaking client
17
+
18
+ Usage:
19
+ npx mikser-io-mcp register claude [options]
20
+ npx mikser-io-mcp register chatgpt --url <URL>
21
+
22
+ Common options:
23
+ --url=<URL> MCP endpoint URL. Default for claude: http://localhost:3001/mcp.
24
+ REQUIRED for chatgpt (must be publicly reachable HTTPS).
25
+ --dry-run Show what would change without writing.
26
+
27
+ Claude-only options:
28
+ --unregister Remove this project's entry from Claude Desktop's config.
29
+ --force Overwrite an existing entry that doesn't match.
30
+
31
+ Run from inside a mikser project directory — the project's package.json
32
+ is read for the connector name.
33
+ `)
34
+ process.exit(code)
35
+ }
36
+
37
+ const [, , cmd, host, ...rest] = process.argv
38
+
39
+ if (!cmd) usage(0)
40
+ if (cmd === '-h' || cmd === '--help') usage(0)
41
+
42
+ if (cmd !== 'register') {
43
+ process.stderr.write(`Unknown command: ${cmd}\n\n`)
44
+ usage(1)
45
+ }
46
+
47
+ if (host === 'claude') {
48
+ await runClaude(rest)
49
+ } else if (host === 'chatgpt') {
50
+ await runChatGPT(rest)
51
+ } else if (!host) {
52
+ process.stderr.write(`'register' needs a target host: claude | chatgpt\n\n`)
53
+ usage(1)
54
+ } else {
55
+ process.stderr.write(`Unknown host: ${host}. Supported: claude, chatgpt.\n\n`)
56
+ usage(1)
57
+ }