mikser-io-mcp 0.2.0 → 0.2.1

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/index.js CHANGED
@@ -655,9 +655,11 @@ 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.
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.
661
663
  export default (core) => {
662
664
  // Use the runtime singleton imported above for substrate-internal
663
665
  // code paths (createMcpSubstrate etc. reference it directly via
@@ -665,10 +667,9 @@ export default (core) => {
665
667
  // they're both mikser-io's exported runtime singleton.
666
668
  const { onLoaded, useLogger } = core
667
669
 
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
- }
670
+ // Default the config block so every downstream `runtime.config.mcp.X`
671
+ // reference Just Works without optional-chaining everywhere.
672
+ runtime.config.mcp = runtime.config.mcp ?? {}
672
673
 
673
674
  // Contribute MCP transport headers to engine's CORS arrays so
674
675
  // browser-side MCP clients (basic-host, mcp-ui, etc.) can read
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "mikser-io-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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
  ],
@@ -0,0 +1,105 @@
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
+ for (const a of args) {
18
+ if (a.startsWith('--url=')) opts.url = a.slice(6)
19
+ else if (a === '--url') opts._expectUrl = true
20
+ else if (opts._expectUrl) { opts.url = a; opts._expectUrl = false }
21
+ }
22
+ return opts
23
+ }
24
+
25
+ function readProjectMeta() {
26
+ const pkgPath = resolve(process.cwd(), 'package.json')
27
+ if (!existsSync(pkgPath)) {
28
+ throw new Error(`No package.json in ${process.cwd()} — run this from inside a mikser project directory.`)
29
+ }
30
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
31
+ if (!pkg.name) throw new Error('package.json has no "name" field — set one before registering.')
32
+ return {
33
+ name: pkg.name,
34
+ description: pkg.description ?? `MCP connector for ${pkg.name}.`,
35
+ }
36
+ }
37
+
38
+ export async function runChatGPT(args) {
39
+ const opts = parseArgs(args)
40
+
41
+ if (!opts.url) {
42
+ process.stderr.write(
43
+ `! ChatGPT needs a publicly-reachable HTTPS URL.
44
+
45
+ OpenAI's servers connect to your MCP endpoint directly — there's no stdio
46
+ bridge, no localhost. You'll need to expose mikser via a tunnel first:
47
+
48
+ ngrok http 3001
49
+ # or:
50
+ cloudflared tunnel --url http://localhost:3001
51
+
52
+ Then re-run with the public URL:
53
+
54
+ npx mikser-io-mcp register chatgpt --url https://YOUR-TUNNEL.example/mcp
55
+
56
+ ChatGPT's Developer mode + custom MCP connectors are currently in BETA
57
+ and require a Plus / Pro / Business / Enterprise / Edu account. See
58
+ https://help.openai.com/en/articles/12584461 for the current state.
59
+ `
60
+ )
61
+ process.exit(1)
62
+ }
63
+
64
+ const { name, description } = readProjectMeta()
65
+
66
+ process.stdout.write(
67
+ `✓ Connector details for "${name}"
68
+
69
+ ChatGPT doesn't have a local config file — connectors are added through
70
+ the UI. Copy the three fields below.
71
+
72
+ Open ChatGPT (Desktop or web) and navigate to:
73
+ Settings → Connectors → Advanced → Developer mode (toggle ON)
74
+ Click + Create
75
+
76
+ Paste these into the form:
77
+
78
+ Name: ${name}
79
+ Description: ${description}
80
+ MCP Server URL: ${opts.url}
81
+ Authentication: None (or your scheme of choice for production)
82
+
83
+ Click Create. Then to use it:
84
+
85
+ 1. Start a NEW chat (saved connectors are off-by-default per chat)
86
+ 2. Open the composer's Developer Mode tool picker
87
+ 3. Enable "${name}"
88
+ 4. Try: "Use mikser_query_entities to show me everything in this catalog"
89
+
90
+ Notes:
91
+ - Mikser must be running and the URL must be reachable from
92
+ OpenAI's servers (not just your machine).
93
+ - Connectors do NOT auto-enable per chat — toggle on for each new
94
+ conversation.
95
+ - Developer mode + arbitrary custom MCP connectors are BETA. Plus /
96
+ Pro / Business / Enterprise / Edu only; Free tier excluded.
97
+ - "ChatGPT Apps" is the GA but read-only variant of MCP integration;
98
+ this script targets the full read+write Developer mode beta.
99
+
100
+ Reference:
101
+ https://help.openai.com/en/articles/12584461
102
+ https://developers.openai.com/apps-sdk/deploy/connect-chatgpt
103
+ `
104
+ )
105
+ }
@@ -0,0 +1,121 @@
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
+ for (const a of args) {
28
+ if (a === '--unregister') opts.unregister = true
29
+ else if (a === '--dry-run') opts.dryRun = true
30
+ else if (a === '--force') opts.force = true
31
+ else if (a.startsWith('--url=')) opts.url = a.slice(6)
32
+ else if (a === '--url') opts._expectUrl = true
33
+ else if (opts._expectUrl) { opts.url = a; opts._expectUrl = false }
34
+ }
35
+ return opts
36
+ }
37
+
38
+ function readProjectName() {
39
+ const pkgPath = resolve(process.cwd(), 'package.json')
40
+ if (!existsSync(pkgPath)) {
41
+ throw new Error(`No package.json in ${process.cwd()} — run this from inside a mikser project directory.`)
42
+ }
43
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
44
+ if (!pkg.name) throw new Error('package.json has no "name" field — set one before registering.')
45
+ return pkg.name
46
+ }
47
+
48
+ export async function runClaude(args) {
49
+ const opts = parseArgs(args)
50
+ const name = readProjectName()
51
+ const url = opts.url ?? DEFAULT_URL
52
+
53
+ const entry = {
54
+ command: 'npx',
55
+ args: ['-y', 'supergateway', '--streamableHttp', url],
56
+ }
57
+
58
+ const path = configPath()
59
+ let config = {}
60
+ let existed = false
61
+
62
+ if (existsSync(path)) {
63
+ existed = true
64
+ try {
65
+ config = JSON.parse(readFileSync(path, 'utf8'))
66
+ } catch (err) {
67
+ 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`)
68
+ process.exit(1)
69
+ }
70
+ }
71
+
72
+ config.mcpServers = config.mcpServers || {}
73
+
74
+ // Unregister branch.
75
+ if (opts.unregister) {
76
+ if (!(name in config.mcpServers)) {
77
+ process.stdout.write(`✓ "${name}" is not registered. Nothing to do.\n`)
78
+ return
79
+ }
80
+ if (opts.dryRun) {
81
+ process.stdout.write(`[dry-run] Would remove "${name}" from ${path}\n`)
82
+ return
83
+ }
84
+ delete config.mcpServers[name]
85
+ writeFileSync(path, JSON.stringify(config, null, 2) + '\n')
86
+ process.stdout.write(`✓ Removed "${name}" from ${path}\n Restart Claude Desktop to apply.\n`)
87
+ return
88
+ }
89
+
90
+ // Idempotent — bail if already matching.
91
+ const existing = config.mcpServers[name]
92
+ if (existing && JSON.stringify(existing) === JSON.stringify(entry)) {
93
+ process.stdout.write(`✓ "${name}" is already registered with the matching config.\n Nothing to change. (Use --force to re-write anyway.)\n`)
94
+ return
95
+ }
96
+ if (existing && !opts.force) {
97
+ 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`)
98
+ process.exit(1)
99
+ }
100
+ if (opts.dryRun) {
101
+ process.stdout.write(`[dry-run] Would ${existing ? 'overwrite' : 'add'} "${name}" in ${path}:\n${JSON.stringify(entry, null, 2)}\n`)
102
+ return
103
+ }
104
+
105
+ config.mcpServers[name] = entry
106
+ mkdirSync(dirname(path), { recursive: true })
107
+ writeFileSync(path, JSON.stringify(config, null, 2) + '\n')
108
+
109
+ process.stdout.write(
110
+ `✓ Registered "${name}" in
111
+ ${path}
112
+ ${existed ? '(merged into existing config)' : '(created new config file)'}
113
+
114
+ Next steps:
115
+ 1. Make sure mikser is running and reachable at ${url}
116
+ 2. Restart Claude Desktop (fully quit, then reopen)
117
+ 3. The mikser tools should appear in Claude's tool list
118
+
119
+ To remove: npx mikser-io-mcp register claude --unregister
120
+ `)
121
+ }
@@ -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
+ }