conduit-mcp 0.1.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 ADDED
@@ -0,0 +1,105 @@
1
+ # conduit-mcp
2
+
3
+ MCP server for [Conduit](https://usecondu.it) — connect any AI agent to your data streams.
4
+
5
+ ## Quick Start
6
+
7
+ Get an API key from [platform.usecondu.it/tokens](https://platform.usecondu.it/tokens), then add to your AI editor config:
8
+
9
+ ### Claude Code / Claude Desktop
10
+
11
+ Add to `~/.claude/settings.json`:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "conduit": {
17
+ "command": "npx",
18
+ "args": ["-y", "conduit-mcp"],
19
+ "env": {
20
+ "CONDUIT_API_KEY": "conduit_sk_..."
21
+ }
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ ### Cursor
28
+
29
+ Add to `.cursor/mcp.json` in your project:
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "conduit": {
35
+ "command": "npx",
36
+ "args": ["-y", "conduit-mcp"],
37
+ "env": {
38
+ "CONDUIT_API_KEY": "conduit_sk_..."
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ### Windsurf
46
+
47
+ Add to `~/.codeium/windsurf/mcp_config.json`:
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "conduit": {
53
+ "command": "npx",
54
+ "args": ["-y", "conduit-mcp"],
55
+ "env": {
56
+ "CONDUIT_API_KEY": "conduit_sk_..."
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Environment Variables
64
+
65
+ | Variable | Required | Default | Description |
66
+ |---|---|---|---|
67
+ | `CONDUIT_API_KEY` | ✅ | — | Your Conduit API key |
68
+ | `CONDUIT_API_URL` | — | `https://api.usecondu.it` | Custom API endpoint |
69
+
70
+ ## Tools
71
+
72
+ | Tool | Description |
73
+ |---|---|
74
+ | `conduit_list_streams` | List all data streams |
75
+ | `conduit_get_schema` | Get schema for a stream (columns, types, codecs) |
76
+ | `conduit_create_stream` | Create a new stream |
77
+ | `conduit_ingest` | Send events to a stream |
78
+ | `conduit_list_events` | Query events with pagination & time range |
79
+ | `conduit_add_forward` | Add a webhook forwarding destination |
80
+ | `conduit_stream_stats` | Get ingestion statistics |
81
+ | `conduit_analyze_schema` | Analyze a JSON payload for optimal schema |
82
+ | `conduit_feedback` | Submit feedback to the Conduit team |
83
+
84
+ ## Resources
85
+
86
+ | URI | Description |
87
+ |---|---|
88
+ | `conduit://streams` | All streams |
89
+ | `conduit://streams/{name}` | Stream details + schema |
90
+ | `conduit://stats` | Platform-wide statistics |
91
+
92
+ ## What is Conduit?
93
+
94
+ Conduit is the lightweight data layer between your services. Send any JSON — structure is inferred, not defined. Schema evolves automatically. Forward to any destination.
95
+
96
+ - **One endpoint, any protocol** — HTTP, WebSocket, MQTT
97
+ - **AI-powered schema detection** — zero configuration
98
+ - **Real-time forwarding** — webhooks, MQTT, more coming
99
+ - **Built for agents** — MCP-native from day one
100
+
101
+ Learn more at [usecondu.it](https://usecondu.it)
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Conduit MCP Server
4
+ *
5
+ * Connect any AI agent to your Conduit data streams via
6
+ * the Model Context Protocol.
7
+ *
8
+ * Environment variables:
9
+ * CONDUIT_API_URL — Conduit API base URL (default: https://api.usecondu.it)
10
+ * CONDUIT_API_KEY — Your Conduit API key (required)
11
+ *
12
+ * Tools:
13
+ * conduit_list_streams — List all streams
14
+ * conduit_get_schema — Get schema for a stream
15
+ * conduit_create_stream — Create a new stream
16
+ * conduit_ingest — Send events to a stream
17
+ * conduit_list_events — Query events with pagination & time range
18
+ * conduit_add_forward — Add a forwarding destination
19
+ * conduit_stream_stats — Get ingestion statistics
20
+ * conduit_analyze_schema — Analyze a JSON payload for optimal schema
21
+ * conduit_feedback — Submit feedback to the Conduit team
22
+ *
23
+ * Resources:
24
+ * conduit://streams — All streams
25
+ * conduit://streams/{name} — Stream details + schema
26
+ * conduit://stats — Platform-wide statistics
27
+ */
28
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Conduit MCP Server
4
+ *
5
+ * Connect any AI agent to your Conduit data streams via
6
+ * the Model Context Protocol.
7
+ *
8
+ * Environment variables:
9
+ * CONDUIT_API_URL — Conduit API base URL (default: https://api.usecondu.it)
10
+ * CONDUIT_API_KEY — Your Conduit API key (required)
11
+ *
12
+ * Tools:
13
+ * conduit_list_streams — List all streams
14
+ * conduit_get_schema — Get schema for a stream
15
+ * conduit_create_stream — Create a new stream
16
+ * conduit_ingest — Send events to a stream
17
+ * conduit_list_events — Query events with pagination & time range
18
+ * conduit_add_forward — Add a forwarding destination
19
+ * conduit_stream_stats — Get ingestion statistics
20
+ * conduit_analyze_schema — Analyze a JSON payload for optimal schema
21
+ * conduit_feedback — Submit feedback to the Conduit team
22
+ *
23
+ * Resources:
24
+ * conduit://streams — All streams
25
+ * conduit://streams/{name} — Stream details + schema
26
+ * conduit://stats — Platform-wide statistics
27
+ */
28
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
29
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
30
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
31
+ const API = process.env.CONDUIT_API_URL || 'https://api.usecondu.it';
32
+ const KEY = process.env.CONDUIT_API_KEY || '';
33
+ if (!KEY) {
34
+ console.error('[conduit-mcp] CONDUIT_API_KEY is required. Get one at https://platform.usecondu.it/tokens');
35
+ process.exit(1);
36
+ }
37
+ async function api(path, options) {
38
+ const headers = {
39
+ 'Content-Type': 'application/json',
40
+ 'Authorization': `Bearer ${KEY}`,
41
+ ...(options?.headers || {}),
42
+ };
43
+ const res = await fetch(`${API}${path}`, { ...options, headers });
44
+ if (!res.ok) {
45
+ const body = await res.text().catch(() => '');
46
+ throw new Error(`API ${res.status}: ${body}`);
47
+ }
48
+ return res.json();
49
+ }
50
+ // Extract tenant prefix from first API call
51
+ let tenantPrefix = '';
52
+ async function getTenantPrefix() {
53
+ if (tenantPrefix)
54
+ return tenantPrefix;
55
+ // Get it from listing streams (the API key scopes to a tenant)
56
+ const streams = await api('/api/v1/streams');
57
+ if (Array.isArray(streams) && streams.length > 0) {
58
+ tenantPrefix = streams[0].tenant_id?.slice(0, 8) || 'unknown';
59
+ }
60
+ else {
61
+ tenantPrefix = 'unknown';
62
+ }
63
+ return tenantPrefix;
64
+ }
65
+ const server = new Server({ name: 'conduit', version: '0.1.0' }, { capabilities: { tools: {}, resources: {} } });
66
+ // === TOOLS ===
67
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
68
+ tools: [
69
+ {
70
+ name: 'conduit_list_streams',
71
+ description: 'List all data streams in your Conduit account',
72
+ inputSchema: { type: 'object', properties: {} },
73
+ },
74
+ {
75
+ name: 'conduit_get_schema',
76
+ description: 'Get the current schema (columns, types, codecs) for a stream',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: { stream: { type: 'string', description: 'Stream name' } },
80
+ required: ['stream'],
81
+ },
82
+ },
83
+ {
84
+ name: 'conduit_create_stream',
85
+ description: 'Create a new data stream. Just send a name — schema is auto-detected from the first event.',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ name: { type: 'string', description: 'Stream name (alphanumeric, underscores, hyphens)' },
90
+ description: { type: 'string', description: 'Optional description' },
91
+ },
92
+ required: ['name'],
93
+ },
94
+ },
95
+ {
96
+ name: 'conduit_ingest',
97
+ description: 'Send one or more events (JSON objects) to a stream. Schema is auto-detected and evolves.',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ stream: { type: 'string', description: 'Stream name' },
102
+ events: {
103
+ oneOf: [
104
+ { type: 'object', description: 'Single event' },
105
+ { type: 'array', items: { type: 'object' }, description: 'Batch of events' },
106
+ ],
107
+ description: 'Event(s) to ingest',
108
+ },
109
+ },
110
+ required: ['stream', 'events'],
111
+ },
112
+ },
113
+ {
114
+ name: 'conduit_list_events',
115
+ description: 'Query events from a stream with optional pagination and time range filters',
116
+ inputSchema: {
117
+ type: 'object',
118
+ properties: {
119
+ stream: { type: 'string', description: 'Stream name' },
120
+ limit: { type: 'number', description: 'Max events to return (default: 50, max: 1000)' },
121
+ offset: { type: 'number', description: 'Offset for pagination' },
122
+ from: { type: 'string', description: 'Start time (ISO 8601)' },
123
+ to: { type: 'string', description: 'End time (ISO 8601)' },
124
+ },
125
+ required: ['stream'],
126
+ },
127
+ },
128
+ {
129
+ name: 'conduit_add_forward',
130
+ description: 'Add a forwarding destination to a stream. Supports HTTP webhooks, MQTT brokers, and WebSocket endpoints. Events are forwarded in real-time.',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ stream: { type: 'string', description: 'Stream name' },
135
+ dest_type: { type: 'string', enum: ['http', 'mqtt', 'websocket'], description: 'Destination type (default: http)' },
136
+ url: { type: 'string', description: 'Webhook or WebSocket URL (for http/websocket types)' },
137
+ broker: { type: 'string', description: 'MQTT broker URL (for mqtt type)' },
138
+ topic: { type: 'string', description: 'MQTT topic (for mqtt type)' },
139
+ auth_method: { type: 'string', enum: ['none', 'bearer', 'hmac', 'api_key', 'basic', 'custom_headers'], description: 'Authentication method (default: none)' },
140
+ auth_token: { type: 'string', description: 'Bearer token (for bearer auth)' },
141
+ hmac_secret: { type: 'string', description: 'HMAC signing secret (for hmac auth)' },
142
+ basic_user: { type: 'string', description: 'Username (for basic auth)' },
143
+ basic_pass: { type: 'string', description: 'Password (for basic auth)' },
144
+ schema_mode: { type: 'string', enum: ['pass-through', 'pinned', 'mapped'], description: 'Schema mode (default: pass-through)' },
145
+ },
146
+ required: ['stream'],
147
+ },
148
+ },
149
+ {
150
+ name: 'conduit_stream_stats',
151
+ description: 'Get ingestion statistics for a stream (event count, rate, schema version)',
152
+ inputSchema: {
153
+ type: 'object',
154
+ properties: { stream: { type: 'string', description: 'Stream name' } },
155
+ required: ['stream'],
156
+ },
157
+ },
158
+ {
159
+ name: 'conduit_analyze_schema',
160
+ description: 'Analyze a JSON payload and get the optimal ClickHouse schema with compression codecs',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ payload: { type: 'object', description: 'Sample JSON payload to analyze' },
165
+ },
166
+ required: ['payload'],
167
+ },
168
+ },
169
+ {
170
+ name: 'conduit_feedback',
171
+ description: 'Submit feedback to the Conduit team — bugs, feature requests, improvements, or praise',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ category: {
176
+ type: 'string',
177
+ enum: ['bug', 'feature', 'improvement', 'general', 'praise'],
178
+ description: 'Feedback category',
179
+ },
180
+ message: { type: 'string', description: 'Your feedback (max 5000 chars)' },
181
+ context: { type: 'object', description: 'Optional context (stream name, error details, etc.)' },
182
+ },
183
+ required: ['category', 'message'],
184
+ },
185
+ },
186
+ ],
187
+ }));
188
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
189
+ const { name, arguments: args } = request.params;
190
+ try {
191
+ switch (name) {
192
+ case 'conduit_list_streams': {
193
+ const streams = await api('/api/v1/streams');
194
+ return { content: [{ type: 'text', text: JSON.stringify(streams, null, 2) }] };
195
+ }
196
+ case 'conduit_get_schema': {
197
+ const data = await api(`/api/v1/streams/${args.stream}`);
198
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
199
+ }
200
+ case 'conduit_create_stream': {
201
+ const data = await api('/api/v1/streams', {
202
+ method: 'POST',
203
+ body: JSON.stringify({ name: args.name, description: args.description || '' }),
204
+ });
205
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
206
+ }
207
+ case 'conduit_ingest': {
208
+ const prefix = await getTenantPrefix();
209
+ const events = Array.isArray(args.events) ? args.events : [args.events];
210
+ const data = await api(`/v1/${prefix}/${args.stream}`, {
211
+ method: 'POST',
212
+ body: JSON.stringify(events.length === 1 ? events[0] : events),
213
+ });
214
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
215
+ }
216
+ case 'conduit_list_events': {
217
+ const params = new URLSearchParams();
218
+ if (args.limit)
219
+ params.set('limit', String(args.limit));
220
+ if (args.offset)
221
+ params.set('offset', String(args.offset));
222
+ if (args.from)
223
+ params.set('from', args.from);
224
+ if (args.to)
225
+ params.set('to', args.to);
226
+ const qs = params.toString();
227
+ const data = await api(`/api/v1/streams/${args.stream}/events${qs ? `?${qs}` : ''}`);
228
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
229
+ }
230
+ case 'conduit_add_forward': {
231
+ const body = {
232
+ type: args.dest_type || 'http',
233
+ schema_mode: args.schema_mode || 'pass-through',
234
+ };
235
+ if (body.type === 'http' || body.type === 'websocket') {
236
+ body.url = args.url;
237
+ }
238
+ if (body.type === 'mqtt') {
239
+ body.broker = args.broker;
240
+ body.topic = args.topic;
241
+ if (args.mqtt_user)
242
+ body.mqtt_user = args.mqtt_user;
243
+ if (args.mqtt_pass)
244
+ body.mqtt_pass = args.mqtt_pass;
245
+ }
246
+ if (args.auth_method && args.auth_method !== 'none') {
247
+ body.auth_method = args.auth_method;
248
+ if (args.auth_token)
249
+ body.auth_token = args.auth_token;
250
+ if (args.hmac_secret)
251
+ body.hmac_secret = args.hmac_secret;
252
+ if (args.basic_user)
253
+ body.basic_user = args.basic_user;
254
+ if (args.basic_pass)
255
+ body.basic_pass = args.basic_pass;
256
+ }
257
+ const data = await api(`/api/v1/streams/${args.stream}/forwards`, {
258
+ method: 'POST',
259
+ body: JSON.stringify(body),
260
+ });
261
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
262
+ }
263
+ case 'conduit_stream_stats': {
264
+ const data = await api(`/api/v1/streams/${args.stream}/stats`);
265
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
266
+ }
267
+ case 'conduit_analyze_schema': {
268
+ const data = await api('/api/v1/analyze', {
269
+ method: 'POST',
270
+ body: JSON.stringify(args.payload),
271
+ });
272
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
273
+ }
274
+ case 'conduit_feedback': {
275
+ const data = await api('/api/v1/feedback', {
276
+ method: 'POST',
277
+ body: JSON.stringify({
278
+ category: args.category,
279
+ message: args.message,
280
+ source: 'mcp',
281
+ context: args.context || {},
282
+ }),
283
+ });
284
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
285
+ }
286
+ default:
287
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
288
+ }
289
+ }
290
+ catch (err) {
291
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
292
+ }
293
+ });
294
+ // === RESOURCES ===
295
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
296
+ resources: [
297
+ { uri: 'conduit://streams', name: 'All Streams', description: 'List of all data streams', mimeType: 'application/json' },
298
+ { uri: 'conduit://stats', name: 'Platform Stats', description: 'Platform-wide statistics', mimeType: 'application/json' },
299
+ ],
300
+ }));
301
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
302
+ const { uri } = request.params;
303
+ if (uri === 'conduit://streams') {
304
+ const streams = await api('/api/v1/streams');
305
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(streams, null, 2) }] };
306
+ }
307
+ const streamMatch = uri.match(/^conduit:\/\/streams\/(.+)$/);
308
+ if (streamMatch) {
309
+ const data = await api(`/api/v1/streams/${streamMatch[1]}`);
310
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data, null, 2) }] };
311
+ }
312
+ if (uri === 'conduit://stats') {
313
+ const streams = await api('/api/v1/streams');
314
+ return {
315
+ contents: [{
316
+ uri,
317
+ mimeType: 'application/json',
318
+ text: JSON.stringify({ totalStreams: Array.isArray(streams) ? streams.length : 0, streams }, null, 2),
319
+ }],
320
+ };
321
+ }
322
+ throw new Error(`Unknown resource: ${uri}`);
323
+ });
324
+ // === START ===
325
+ async function main() {
326
+ const transport = new StdioServerTransport();
327
+ await server.connect(transport);
328
+ console.error('[conduit-mcp] Connected — ready for requests');
329
+ }
330
+ main().catch((err) => {
331
+ console.error('[conduit-mcp] Fatal:', err);
332
+ process.exit(1);
333
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "conduit-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Conduit — connect any AI agent to your data streams",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "conduit-mcp": "dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "conduit",
18
+ "ai",
19
+ "data-pipeline",
20
+ "model-context-protocol",
21
+ "iot",
22
+ "streams"
23
+ ],
24
+ "author": "Conduit <hello@usecondu.it>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/useconduit/mcp"
29
+ },
30
+ "homepage": "https://usecondu.it",
31
+ "bugs": {
32
+ "url": "https://github.com/useconduit/mcp/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.27.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.3.3",
42
+ "tsx": "^4.19.0",
43
+ "typescript": "^5.7.0"
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Conduit MCP Server
5
+ *
6
+ * Connect any AI agent to your Conduit data streams via
7
+ * the Model Context Protocol.
8
+ *
9
+ * Environment variables:
10
+ * CONDUIT_API_URL — Conduit API base URL (default: https://api.usecondu.it)
11
+ * CONDUIT_API_KEY — Your Conduit API key (required)
12
+ *
13
+ * Tools:
14
+ * conduit_list_streams — List all streams
15
+ * conduit_get_schema — Get schema for a stream
16
+ * conduit_create_stream — Create a new stream
17
+ * conduit_ingest — Send events to a stream
18
+ * conduit_list_events — Query events with pagination & time range
19
+ * conduit_add_forward — Add a forwarding destination
20
+ * conduit_stream_stats — Get ingestion statistics
21
+ * conduit_analyze_schema — Analyze a JSON payload for optimal schema
22
+ * conduit_feedback — Submit feedback to the Conduit team
23
+ *
24
+ * Resources:
25
+ * conduit://streams — All streams
26
+ * conduit://streams/{name} — Stream details + schema
27
+ * conduit://stats — Platform-wide statistics
28
+ */
29
+
30
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
31
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
32
+ import {
33
+ CallToolRequestSchema,
34
+ ListToolsRequestSchema,
35
+ ListResourcesRequestSchema,
36
+ ReadResourceRequestSchema,
37
+ } from '@modelcontextprotocol/sdk/types.js';
38
+
39
+ const API = process.env.CONDUIT_API_URL || 'https://api.usecondu.it';
40
+ const KEY = process.env.CONDUIT_API_KEY || '';
41
+
42
+ if (!KEY) {
43
+ console.error('[conduit-mcp] CONDUIT_API_KEY is required. Get one at https://platform.usecondu.it/tokens');
44
+ process.exit(1);
45
+ }
46
+
47
+ async function api(path: string, options?: RequestInit) {
48
+ const headers: Record<string, string> = {
49
+ 'Content-Type': 'application/json',
50
+ 'Authorization': `Bearer ${KEY}`,
51
+ ...(options?.headers as Record<string, string> || {}),
52
+ };
53
+ const res = await fetch(`${API}${path}`, { ...options, headers });
54
+ if (!res.ok) {
55
+ const body = await res.text().catch(() => '');
56
+ throw new Error(`API ${res.status}: ${body}`);
57
+ }
58
+ return res.json();
59
+ }
60
+
61
+ // Extract tenant prefix from first API call
62
+ let tenantPrefix = '';
63
+
64
+ async function getTenantPrefix(): Promise<string> {
65
+ if (tenantPrefix) return tenantPrefix;
66
+ // Get it from listing streams (the API key scopes to a tenant)
67
+ const streams = await api('/api/v1/streams');
68
+ if (Array.isArray(streams) && streams.length > 0) {
69
+ tenantPrefix = (streams[0] as any).tenant_id?.slice(0, 8) || 'unknown';
70
+ } else {
71
+ tenantPrefix = 'unknown';
72
+ }
73
+ return tenantPrefix;
74
+ }
75
+
76
+ const server = new Server(
77
+ { name: 'conduit', version: '0.1.0' },
78
+ { capabilities: { tools: {}, resources: {} } }
79
+ );
80
+
81
+ // === TOOLS ===
82
+
83
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
84
+ tools: [
85
+ {
86
+ name: 'conduit_list_streams',
87
+ description: 'List all data streams in your Conduit account',
88
+ inputSchema: { type: 'object' as const, properties: {} },
89
+ },
90
+ {
91
+ name: 'conduit_get_schema',
92
+ description: 'Get the current schema (columns, types, codecs) for a stream',
93
+ inputSchema: {
94
+ type: 'object' as const,
95
+ properties: { stream: { type: 'string', description: 'Stream name' } },
96
+ required: ['stream'],
97
+ },
98
+ },
99
+ {
100
+ name: 'conduit_create_stream',
101
+ description: 'Create a new data stream. Just send a name — schema is auto-detected from the first event.',
102
+ inputSchema: {
103
+ type: 'object' as const,
104
+ properties: {
105
+ name: { type: 'string', description: 'Stream name (alphanumeric, underscores, hyphens)' },
106
+ description: { type: 'string', description: 'Optional description' },
107
+ },
108
+ required: ['name'],
109
+ },
110
+ },
111
+ {
112
+ name: 'conduit_ingest',
113
+ description: 'Send one or more events (JSON objects) to a stream. Schema is auto-detected and evolves.',
114
+ inputSchema: {
115
+ type: 'object' as const,
116
+ properties: {
117
+ stream: { type: 'string', description: 'Stream name' },
118
+ events: {
119
+ oneOf: [
120
+ { type: 'object', description: 'Single event' },
121
+ { type: 'array', items: { type: 'object' }, description: 'Batch of events' },
122
+ ],
123
+ description: 'Event(s) to ingest',
124
+ },
125
+ },
126
+ required: ['stream', 'events'],
127
+ },
128
+ },
129
+ {
130
+ name: 'conduit_list_events',
131
+ description: 'Query events from a stream with optional pagination and time range filters',
132
+ inputSchema: {
133
+ type: 'object' as const,
134
+ properties: {
135
+ stream: { type: 'string', description: 'Stream name' },
136
+ limit: { type: 'number', description: 'Max events to return (default: 50, max: 1000)' },
137
+ offset: { type: 'number', description: 'Offset for pagination' },
138
+ from: { type: 'string', description: 'Start time (ISO 8601)' },
139
+ to: { type: 'string', description: 'End time (ISO 8601)' },
140
+ },
141
+ required: ['stream'],
142
+ },
143
+ },
144
+ {
145
+ name: 'conduit_add_forward',
146
+ description: 'Add a forwarding destination to a stream. Supports HTTP webhooks, MQTT brokers, and WebSocket endpoints. Events are forwarded in real-time.',
147
+ inputSchema: {
148
+ type: 'object' as const,
149
+ properties: {
150
+ stream: { type: 'string', description: 'Stream name' },
151
+ dest_type: { type: 'string', enum: ['http', 'mqtt', 'websocket'], description: 'Destination type (default: http)' },
152
+ url: { type: 'string', description: 'Webhook or WebSocket URL (for http/websocket types)' },
153
+ broker: { type: 'string', description: 'MQTT broker URL (for mqtt type)' },
154
+ topic: { type: 'string', description: 'MQTT topic (for mqtt type)' },
155
+ auth_method: { type: 'string', enum: ['none', 'bearer', 'hmac', 'api_key', 'basic', 'custom_headers'], description: 'Authentication method (default: none)' },
156
+ auth_token: { type: 'string', description: 'Bearer token (for bearer auth)' },
157
+ hmac_secret: { type: 'string', description: 'HMAC signing secret (for hmac auth)' },
158
+ basic_user: { type: 'string', description: 'Username (for basic auth)' },
159
+ basic_pass: { type: 'string', description: 'Password (for basic auth)' },
160
+ schema_mode: { type: 'string', enum: ['pass-through', 'pinned', 'mapped'], description: 'Schema mode (default: pass-through)' },
161
+ },
162
+ required: ['stream'],
163
+ },
164
+ },
165
+ {
166
+ name: 'conduit_stream_stats',
167
+ description: 'Get ingestion statistics for a stream (event count, rate, schema version)',
168
+ inputSchema: {
169
+ type: 'object' as const,
170
+ properties: { stream: { type: 'string', description: 'Stream name' } },
171
+ required: ['stream'],
172
+ },
173
+ },
174
+ {
175
+ name: 'conduit_analyze_schema',
176
+ description: 'Analyze a JSON payload and get the optimal ClickHouse schema with compression codecs',
177
+ inputSchema: {
178
+ type: 'object' as const,
179
+ properties: {
180
+ payload: { type: 'object', description: 'Sample JSON payload to analyze' },
181
+ },
182
+ required: ['payload'],
183
+ },
184
+ },
185
+ {
186
+ name: 'conduit_feedback',
187
+ description: 'Submit feedback to the Conduit team — bugs, feature requests, improvements, or praise',
188
+ inputSchema: {
189
+ type: 'object' as const,
190
+ properties: {
191
+ category: {
192
+ type: 'string',
193
+ enum: ['bug', 'feature', 'improvement', 'general', 'praise'],
194
+ description: 'Feedback category',
195
+ },
196
+ message: { type: 'string', description: 'Your feedback (max 5000 chars)' },
197
+ context: { type: 'object', description: 'Optional context (stream name, error details, etc.)' },
198
+ },
199
+ required: ['category', 'message'],
200
+ },
201
+ },
202
+ ],
203
+ }));
204
+
205
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
206
+ const { name, arguments: args } = request.params;
207
+
208
+ try {
209
+ switch (name) {
210
+ case 'conduit_list_streams': {
211
+ const streams = await api('/api/v1/streams');
212
+ return { content: [{ type: 'text', text: JSON.stringify(streams, null, 2) }] };
213
+ }
214
+
215
+ case 'conduit_get_schema': {
216
+ const data = await api(`/api/v1/streams/${args!.stream}`);
217
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
218
+ }
219
+
220
+ case 'conduit_create_stream': {
221
+ const data = await api('/api/v1/streams', {
222
+ method: 'POST',
223
+ body: JSON.stringify({ name: args!.name, description: args!.description || '' }),
224
+ });
225
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
226
+ }
227
+
228
+ case 'conduit_ingest': {
229
+ const prefix = await getTenantPrefix();
230
+ const events = Array.isArray(args!.events) ? args!.events : [args!.events];
231
+ const data = await api(`/v1/${prefix}/${args!.stream}`, {
232
+ method: 'POST',
233
+ body: JSON.stringify(events.length === 1 ? events[0] : events),
234
+ });
235
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
236
+ }
237
+
238
+ case 'conduit_list_events': {
239
+ const params = new URLSearchParams();
240
+ if (args!.limit) params.set('limit', String(args!.limit));
241
+ if (args!.offset) params.set('offset', String(args!.offset));
242
+ if (args!.from) params.set('from', args!.from as string);
243
+ if (args!.to) params.set('to', args!.to as string);
244
+ const qs = params.toString();
245
+ const data = await api(`/api/v1/streams/${args!.stream}/events${qs ? `?${qs}` : ''}`);
246
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
247
+ }
248
+
249
+ case 'conduit_add_forward': {
250
+ const body: Record<string, any> = {
251
+ type: args!.dest_type || 'http',
252
+ schema_mode: args!.schema_mode || 'pass-through',
253
+ };
254
+ if (body.type === 'http' || body.type === 'websocket') {
255
+ body.url = args!.url;
256
+ }
257
+ if (body.type === 'mqtt') {
258
+ body.broker = args!.broker;
259
+ body.topic = args!.topic;
260
+ if (args!.mqtt_user) body.mqtt_user = args!.mqtt_user;
261
+ if (args!.mqtt_pass) body.mqtt_pass = args!.mqtt_pass;
262
+ }
263
+ if (args!.auth_method && args!.auth_method !== 'none') {
264
+ body.auth_method = args!.auth_method;
265
+ if (args!.auth_token) body.auth_token = args!.auth_token;
266
+ if (args!.hmac_secret) body.hmac_secret = args!.hmac_secret;
267
+ if (args!.basic_user) body.basic_user = args!.basic_user;
268
+ if (args!.basic_pass) body.basic_pass = args!.basic_pass;
269
+ }
270
+ const data = await api(`/api/v1/streams/${args!.stream}/forwards`, {
271
+ method: 'POST',
272
+ body: JSON.stringify(body),
273
+ });
274
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
275
+ }
276
+
277
+ case 'conduit_stream_stats': {
278
+ const data = await api(`/api/v1/streams/${args!.stream}/stats`);
279
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
280
+ }
281
+
282
+ case 'conduit_analyze_schema': {
283
+ const data = await api('/api/v1/analyze', {
284
+ method: 'POST',
285
+ body: JSON.stringify(args!.payload),
286
+ });
287
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
288
+ }
289
+
290
+ case 'conduit_feedback': {
291
+ const data = await api('/api/v1/feedback', {
292
+ method: 'POST',
293
+ body: JSON.stringify({
294
+ category: args!.category,
295
+ message: args!.message,
296
+ source: 'mcp',
297
+ context: args!.context || {},
298
+ }),
299
+ });
300
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
301
+ }
302
+
303
+ default:
304
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
305
+ }
306
+ } catch (err: any) {
307
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
308
+ }
309
+ });
310
+
311
+ // === RESOURCES ===
312
+
313
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
314
+ resources: [
315
+ { uri: 'conduit://streams', name: 'All Streams', description: 'List of all data streams', mimeType: 'application/json' },
316
+ { uri: 'conduit://stats', name: 'Platform Stats', description: 'Platform-wide statistics', mimeType: 'application/json' },
317
+ ],
318
+ }));
319
+
320
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
321
+ const { uri } = request.params;
322
+
323
+ if (uri === 'conduit://streams') {
324
+ const streams = await api('/api/v1/streams');
325
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(streams, null, 2) }] };
326
+ }
327
+
328
+ const streamMatch = uri.match(/^conduit:\/\/streams\/(.+)$/);
329
+ if (streamMatch) {
330
+ const data = await api(`/api/v1/streams/${streamMatch[1]}`);
331
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data, null, 2) }] };
332
+ }
333
+
334
+ if (uri === 'conduit://stats') {
335
+ const streams = await api('/api/v1/streams');
336
+ return {
337
+ contents: [{
338
+ uri,
339
+ mimeType: 'application/json',
340
+ text: JSON.stringify({ totalStreams: Array.isArray(streams) ? streams.length : 0, streams }, null, 2),
341
+ }],
342
+ };
343
+ }
344
+
345
+ throw new Error(`Unknown resource: ${uri}`);
346
+ });
347
+
348
+ // === START ===
349
+
350
+ async function main() {
351
+ const transport = new StdioServerTransport();
352
+ await server.connect(transport);
353
+ console.error('[conduit-mcp] Connected — ready for requests');
354
+ }
355
+
356
+ main().catch((err) => {
357
+ console.error('[conduit-mcp] Fatal:', err);
358
+ process.exit(1);
359
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }