@datanimbus/dnio-mcp 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/Dockerfile +20 -0
- package/docs/README.md +35 -0
- package/docs/architecture.md +171 -0
- package/docs/authentication.md +74 -0
- package/docs/tools/apps.md +59 -0
- package/docs/tools/connectors.md +76 -0
- package/docs/tools/data-pipes.md +286 -0
- package/docs/tools/data-services.md +105 -0
- package/docs/tools/deployment-groups.md +152 -0
- package/docs/tools/plugins.md +94 -0
- package/docs/tools/records.md +97 -0
- package/docs/workflows.md +195 -0
- package/env.example +16 -0
- package/package.json +43 -0
- package/readme.md +144 -0
- package/src/clients/api-keys.js +10 -0
- package/src/clients/apps.js +13 -0
- package/src/clients/base-client.js +78 -0
- package/src/clients/bots.js +10 -0
- package/src/clients/connectors.js +30 -0
- package/src/clients/data-formats.js +40 -0
- package/src/clients/data-pipes.js +33 -0
- package/src/clients/deployment-groups.js +59 -0
- package/src/clients/formulas.js +10 -0
- package/src/clients/functions.js +10 -0
- package/src/clients/plugins.js +39 -0
- package/src/clients/records.js +51 -0
- package/src/clients/services.js +63 -0
- package/src/clients/user-groups.js +10 -0
- package/src/clients/users.js +10 -0
- package/src/examples/ai-sdk-client.js +165 -0
- package/src/examples/claude_desktop_config.json +34 -0
- package/src/examples/express-integration.js +181 -0
- package/src/index.js +283 -0
- package/src/schemas/schema-converter.js +179 -0
- package/src/services/auth-manager.js +277 -0
- package/src/services/dnio-client.js +40 -0
- package/src/services/service-registry.js +150 -0
- package/src/services/session-manager.js +161 -0
- package/src/stdio-bridge.js +185 -0
- package/src/tools/_helpers.js +32 -0
- package/src/tools/api-keys.js +5 -0
- package/src/tools/apps.js +185 -0
- package/src/tools/bots.js +5 -0
- package/src/tools/connectors.js +165 -0
- package/src/tools/data-formats.js +806 -0
- package/src/tools/data-pipes.js +1305 -0
- package/src/tools/data-service-registry.js +500 -0
- package/src/tools/deployment-groups.js +511 -0
- package/src/tools/formulas.js +5 -0
- package/src/tools/functions.js +5 -0
- package/src/tools/mcp-tools-registry.js +38 -0
- package/src/tools/plugins.js +250 -0
- package/src/tools/records.js +217 -0
- package/src/tools/services.js +476 -0
- package/src/tools/user-groups.js +5 -0
- package/src/tools/users.js +5 -0
- package/src/utils/constants.js +135 -0
- package/src/utils/logger.js +63 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Express Agent Route Integration
|
|
5
|
+
*
|
|
6
|
+
* Shows how to integrate the DNIO MCP server tools into your existing
|
|
7
|
+
* Express-based agent service (the one using streamText + SSE).
|
|
8
|
+
*
|
|
9
|
+
* This is the bridge between your current architecture and MCP.
|
|
10
|
+
* You can use it alongside your existing tool-registry pattern.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const {createMCPClient} = require('@ai-sdk/mcp');
|
|
14
|
+
const logger = require('../utils/logger');
|
|
15
|
+
|
|
16
|
+
// ─── MCP Client Singleton ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
let _mcpClient = null;
|
|
19
|
+
let _mcpTools = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get or create a singleton MCP client connected to the DNIO MCP server.
|
|
23
|
+
* For HTTP transport: connects to a running MCP server.
|
|
24
|
+
* For stdio: spawns the MCP server as a subprocess.
|
|
25
|
+
*/
|
|
26
|
+
async function getMCPTools() {
|
|
27
|
+
if (_mcpTools) return _mcpTools;
|
|
28
|
+
|
|
29
|
+
const mcpUrl = process.env.MCP_SERVER_URL;
|
|
30
|
+
|
|
31
|
+
if (mcpUrl) {
|
|
32
|
+
// Remote HTTP MCP server
|
|
33
|
+
_mcpClient = await createMCPClient({
|
|
34
|
+
transport: {type: 'http', url: mcpUrl}
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
// Local stdio MCP server (spawn as child process)
|
|
38
|
+
const {Experimental_StdioMCPTransport} = require('@ai-sdk/mcp/mcp-stdio');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
_mcpClient = await createMCPClient({
|
|
42
|
+
transport: new Experimental_StdioMCPTransport({
|
|
43
|
+
command: 'node',
|
|
44
|
+
args: [path.resolve(__dirname, '../index.js')],
|
|
45
|
+
env: {...process.env, TRANSPORT: 'stdio'}
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_mcpTools = await _mcpClient.tools();
|
|
51
|
+
logger.info(`Loaded ${Object.keys(_mcpTools).length} DNIO MCP tools`);
|
|
52
|
+
return _mcpTools;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Refresh MCP tools (call after data services change).
|
|
57
|
+
*/
|
|
58
|
+
async function refreshMCPTools() {
|
|
59
|
+
if (_mcpClient) {
|
|
60
|
+
await _mcpClient.close();
|
|
61
|
+
_mcpClient = null;
|
|
62
|
+
_mcpTools = null;
|
|
63
|
+
}
|
|
64
|
+
return getMCPTools();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Graceful shutdown.
|
|
69
|
+
*/
|
|
70
|
+
async function closeMCPClient() {
|
|
71
|
+
if (_mcpClient) {
|
|
72
|
+
await _mcpClient.close();
|
|
73
|
+
_mcpClient = null;
|
|
74
|
+
_mcpTools = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Integration with your existing agent service ────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Example: Merge MCP tools with your existing tool registry.
|
|
82
|
+
*
|
|
83
|
+
* In your existing agent code, you have:
|
|
84
|
+
* const {buildAISDKTools} = require('../tools/tool-registry');
|
|
85
|
+
*
|
|
86
|
+
* Replace or augment it with:
|
|
87
|
+
* const allTools = await buildCombinedTools(existingTools);
|
|
88
|
+
*/
|
|
89
|
+
async function buildCombinedTools(existingTools = {}) {
|
|
90
|
+
try {
|
|
91
|
+
const mcpTools = await getMCPTools();
|
|
92
|
+
return {
|
|
93
|
+
...existingTools,
|
|
94
|
+
...mcpTools
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.error('Failed to load MCP tools, using existing tools only', {error: error.message});
|
|
98
|
+
return existingTools;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Example: Drop-in agent route handler ────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Example Express route handler that uses MCP tools with streamText.
|
|
106
|
+
*
|
|
107
|
+
* Mount this in your Express app:
|
|
108
|
+
* const {agentHandler} = require('./mcp-integration/express-integration');
|
|
109
|
+
* app.post('/api/agent/chat', agentHandler);
|
|
110
|
+
*/
|
|
111
|
+
async function agentHandler(req, res) {
|
|
112
|
+
const {streamText} = require('ai');
|
|
113
|
+
const model = _getModel(); // Use your existing model setup
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const tools = await buildCombinedTools();
|
|
117
|
+
|
|
118
|
+
const result = streamText({
|
|
119
|
+
model,
|
|
120
|
+
tools,
|
|
121
|
+
maxSteps: 10,
|
|
122
|
+
system: `You are a helpful assistant for the DataNimbus BaaS platform.
|
|
123
|
+
You can manage data across multiple data services using the available tools.
|
|
124
|
+
Use list_data_services to discover available services.
|
|
125
|
+
Always explain what you're doing before making changes.`,
|
|
126
|
+
messages: req.body.messages || [{role: 'user', content: req.body.prompt}]
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// SSE streaming response
|
|
130
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
131
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
132
|
+
res.setHeader('Connection', 'keep-alive');
|
|
133
|
+
|
|
134
|
+
for await (const chunk of result.textStream) {
|
|
135
|
+
res.write(`data: ${JSON.stringify({type: 'text', content: chunk})}\n\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
res.write(`data: ${JSON.stringify({type: 'done'})}\n\n`);
|
|
139
|
+
res.end();
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logger.error('Agent handler error', {error: error.message});
|
|
143
|
+
if (!res.headersSent) {
|
|
144
|
+
res.status(500).json({error: error.message});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Helper: create model based on env (mirrors your existing config pattern)
|
|
150
|
+
function _getModel() {
|
|
151
|
+
const provider = process.env.AI_PROVIDER || 'vertex';
|
|
152
|
+
|
|
153
|
+
switch (provider) {
|
|
154
|
+
case 'vertex': {
|
|
155
|
+
const {createVertex} = require('@ai-sdk/google-vertex');
|
|
156
|
+
return createVertex({
|
|
157
|
+
project: process.env.GOOGLE_VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT,
|
|
158
|
+
location: process.env.GOOGLE_VERTEX_LOCATION || 'us-central1'
|
|
159
|
+
})(process.env.AI_MODEL || 'gemini-2.5-flash');
|
|
160
|
+
}
|
|
161
|
+
case 'openai': {
|
|
162
|
+
const {createOpenAI} = require('@ai-sdk/openai');
|
|
163
|
+
return createOpenAI({apiKey: process.env.OPENAI_API_KEY})(process.env.AI_MODEL || 'gpt-4o');
|
|
164
|
+
}
|
|
165
|
+
case 'ollama': {
|
|
166
|
+
const {createOpenAI} = require('@ai-sdk/openai');
|
|
167
|
+
return createOpenAI({
|
|
168
|
+
baseURL: process.env.OLLAMA_BASE_URL || 'http://localhost:11434/v1',
|
|
169
|
+
apiKey: 'ollama'
|
|
170
|
+
})(process.env.AI_MODEL || 'llama3.1');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
getMCPTools,
|
|
177
|
+
refreshMCPTools,
|
|
178
|
+
closeMCPClient,
|
|
179
|
+
buildCombinedTools,
|
|
180
|
+
agentHandler
|
|
181
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
require('dotenv').config({quiet: true});
|
|
5
|
+
|
|
6
|
+
// Catch unhandled errors before they hit stdout
|
|
7
|
+
process.on('uncaughtException', (err) => {
|
|
8
|
+
process.stderr.write(`[FATAL] Uncaught exception: ${err.message}\n${err.stack}\n`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
process.on('unhandledRejection', (reason) => {
|
|
13
|
+
const msg = reason instanceof Error
|
|
14
|
+
? `${reason.message}\n${reason.stack}`
|
|
15
|
+
: String(reason);
|
|
16
|
+
process.stderr.write(`[FATAL] Unhandled rejection: ${msg}\n`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const {McpServer} = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
20
|
+
const {StdioServerTransport} = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
21
|
+
const {StreamableHTTPServerTransport} = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
22
|
+
const express = require('express');
|
|
23
|
+
|
|
24
|
+
const logger = require('./utils/logger');
|
|
25
|
+
const DNIOClient = require('./services/dnio-client');
|
|
26
|
+
const AuthManager = require('./services/auth-manager');
|
|
27
|
+
const ServiceRegistry = require('./services/service-registry');
|
|
28
|
+
const SessionManager = require('./services/session-manager');
|
|
29
|
+
const registerAllTools = require('./tools/mcp-tools-registry');
|
|
30
|
+
|
|
31
|
+
// Configuration — every credential is required from env, no hardcoded defaults.
|
|
32
|
+
const required = (key) => {
|
|
33
|
+
const v = process.env[key];
|
|
34
|
+
if (!v) {
|
|
35
|
+
process.stderr.write(`[FATAL] Missing required env var: ${key}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
return v;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const config = {
|
|
42
|
+
dnio: {
|
|
43
|
+
baseUrl: required('DNIO_BASE_URL'),
|
|
44
|
+
auth: {
|
|
45
|
+
username: required('DNIO_USERNAME'),
|
|
46
|
+
password: required('DNIO_PASSWORD'),
|
|
47
|
+
},
|
|
48
|
+
tokenTTL: parseInt(process.env.DNIO_TOKEN_TTL || '1800', 10),
|
|
49
|
+
namespace: process.env.DNIO_NAMESPACE || 'DNIO',
|
|
50
|
+
},
|
|
51
|
+
transport: process.env.TRANSPORT || 'stdio',
|
|
52
|
+
httpPort: parseInt(process.env.MCP_PORT || '3100'),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// MCP server factory
|
|
56
|
+
function createMCPServer() {
|
|
57
|
+
return new McpServer({
|
|
58
|
+
name: 'dnio-mcp-server',
|
|
59
|
+
version: '2.0.0'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// STDIO transport
|
|
64
|
+
async function runStdio() {
|
|
65
|
+
|
|
66
|
+
const dnioClient = new DNIOClient({baseUrl: config.dnio.baseUrl});
|
|
67
|
+
|
|
68
|
+
const authManager = new AuthManager(dnioClient, {
|
|
69
|
+
username: config.dnio.auth.username,
|
|
70
|
+
password: config.dnio.auth.password,
|
|
71
|
+
namespace: config.dnio.namespace,
|
|
72
|
+
tokenTTL: config.dnio.tokenTTL,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await authManager.initialize();
|
|
76
|
+
|
|
77
|
+
const userToken = await authManager.getUserToken();
|
|
78
|
+
const mcpCreds = authManager.getMcpUserCredentials();
|
|
79
|
+
|
|
80
|
+
const userContext = {
|
|
81
|
+
email: mcpCreds.email,
|
|
82
|
+
password: mcpCreds.password,
|
|
83
|
+
token: userToken,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const registry = new ServiceRegistry(dnioClient);
|
|
87
|
+
const server = createMCPServer();
|
|
88
|
+
|
|
89
|
+
registerAllTools(server, dnioClient, authManager, registry, userContext);
|
|
90
|
+
|
|
91
|
+
const transport = new StdioServerTransport();
|
|
92
|
+
await server.connect(transport);
|
|
93
|
+
|
|
94
|
+
logger.info('DNIO MCP server running on stdio');
|
|
95
|
+
|
|
96
|
+
process.on('SIGINT', () => {
|
|
97
|
+
authManager.destroy();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// HTTP transport
|
|
103
|
+
async function runHTTP() {
|
|
104
|
+
|
|
105
|
+
const adminClient = new DNIOClient({baseUrl: config.dnio.baseUrl});
|
|
106
|
+
|
|
107
|
+
const authManager = new AuthManager(adminClient, {
|
|
108
|
+
username: config.dnio.auth.username,
|
|
109
|
+
password: config.dnio.auth.password,
|
|
110
|
+
namespace: config.dnio.namespace,
|
|
111
|
+
tokenTTL: config.dnio.tokenTTL,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await authManager.initialize();
|
|
115
|
+
|
|
116
|
+
const sessionManager = new SessionManager();
|
|
117
|
+
const app = express();
|
|
118
|
+
app.use(express.json());
|
|
119
|
+
|
|
120
|
+
// CORS
|
|
121
|
+
app.use((req, res, next) => {
|
|
122
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
123
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
|
|
124
|
+
res.header(
|
|
125
|
+
'Access-Control-Allow-Headers',
|
|
126
|
+
'Content-Type, Authorization, Mcp-Session-Id, x-dnio-username, x-dnio-password, x-mcp-proxy-auth'
|
|
127
|
+
);
|
|
128
|
+
res.header('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
129
|
+
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
|
130
|
+
next();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ─── Skip OAuth discovery (server is authless) ─────────────
|
|
134
|
+
app.get('/.well-known/oauth-authorization-server', (req, res) => {
|
|
135
|
+
res.status(404).json({error: 'This server is authless. No OAuth required.'});
|
|
136
|
+
});
|
|
137
|
+
app.get('/.well-known/oauth-protected-resource', (req, res) => {
|
|
138
|
+
res.status(404).json({error: 'This server is authless. No OAuth required.'});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Health
|
|
142
|
+
app.get('/health', (req, res) => {
|
|
143
|
+
res.json({
|
|
144
|
+
status: 'ok',
|
|
145
|
+
server: 'dnio-mcp-server',
|
|
146
|
+
version: '2.0.0',
|
|
147
|
+
transport: 'http',
|
|
148
|
+
activeSessions: sessionManager.activeCount,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Sessions debug
|
|
153
|
+
app.get('/sessions', (req, res) => {
|
|
154
|
+
if (req.query.key !== config.dnio.auth.password) {
|
|
155
|
+
return res.status(401).json({error: 'Unauthorized'});
|
|
156
|
+
}
|
|
157
|
+
res.json({sessions: sessionManager.getSummary()});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// MCP POST
|
|
161
|
+
app.post('/mcp', async (req, res) => {
|
|
162
|
+
try {
|
|
163
|
+
const mcpSessionId = req.headers['mcp-session-id'];
|
|
164
|
+
|
|
165
|
+
if (mcpSessionId) {
|
|
166
|
+
const session = sessionManager.getSession(mcpSessionId);
|
|
167
|
+
if (session && session.transport) {
|
|
168
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const userToken = await authManager.getUserToken();
|
|
174
|
+
const mcpCreds = authManager.getMcpUserCredentials();
|
|
175
|
+
|
|
176
|
+
const session = sessionManager.createSession({
|
|
177
|
+
email: mcpCreds.email,
|
|
178
|
+
dnioToken: userToken,
|
|
179
|
+
tokenExpiresAt: authManager.userExpiresAt,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const userClient = new DNIOClient({
|
|
183
|
+
baseUrl: config.dnio.baseUrl,
|
|
184
|
+
token: userToken
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const registry = new ServiceRegistry(userClient);
|
|
188
|
+
|
|
189
|
+
const userContext = {
|
|
190
|
+
email: mcpCreds.email,
|
|
191
|
+
password: mcpCreds.password,
|
|
192
|
+
token: userToken,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const mcpServer = createMCPServer();
|
|
196
|
+
registerAllTools(mcpServer, userClient, authManager, registry, userContext);
|
|
197
|
+
|
|
198
|
+
const transport = new StreamableHTTPServerTransport({
|
|
199
|
+
sessionIdGenerator: () => session.sessionId,
|
|
200
|
+
enableJsonResponse: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
session.transport = transport;
|
|
204
|
+
session.server = mcpServer;
|
|
205
|
+
session.registry = registry;
|
|
206
|
+
|
|
207
|
+
transport.onclose = () => {
|
|
208
|
+
if (sessionManager.getSession(session.sessionId)) {
|
|
209
|
+
sessionManager.destroySession(session.sessionId);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
await mcpServer.connect(transport);
|
|
214
|
+
await transport.handleRequest(req, res, req.body);
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.error('MCP request failed', {error: error.message});
|
|
218
|
+
if (!res.headersSent) {
|
|
219
|
+
res.status(500).json({error: 'Internal server error'});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// MCP GET (SSE)
|
|
225
|
+
app.get('/mcp', async (req, res) => {
|
|
226
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
227
|
+
if (!sessionId) {
|
|
228
|
+
return res.status(400).json({error: 'Mcp-Session-Id header required'});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const session = sessionManager.getSession(sessionId);
|
|
232
|
+
if (!session || !session.transport) {
|
|
233
|
+
return res.status(404).json({error: 'Session not found'});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await session.transport.handleRequest(req, res);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// MCP DELETE
|
|
240
|
+
app.delete('/mcp', async (req, res) => {
|
|
241
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
242
|
+
|
|
243
|
+
if (sessionId) {
|
|
244
|
+
const session = sessionManager.getSession(sessionId);
|
|
245
|
+
if (session && session.transport) {
|
|
246
|
+
await session.transport.handleRequest(req, res);
|
|
247
|
+
sessionManager.destroySession(sessionId);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
res.status(404).json({error: 'Session not found'});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
app.listen(config.httpPort, () => {
|
|
256
|
+
logger.info(`DNIO MCP server on http://localhost:${config.httpPort}/mcp`);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const shutdown = () => {
|
|
260
|
+
sessionManager.shutdown();
|
|
261
|
+
authManager.destroy();
|
|
262
|
+
process.exit(0);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
process.on('SIGINT', shutdown);
|
|
266
|
+
process.on('SIGTERM', shutdown);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Start
|
|
270
|
+
const transportType = config.transport;
|
|
271
|
+
logger.info(`Starting DNIO MCP server v1.0.0 (transport: ${transportType})`);
|
|
272
|
+
|
|
273
|
+
if (transportType === 'http') {
|
|
274
|
+
runHTTP().catch(err => {
|
|
275
|
+
logger.error('Startup failed', {error: err.message});
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
runStdio().catch(err => {
|
|
280
|
+
logger.error('Startup failed', {error: err.message});
|
|
281
|
+
process.exit(1);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {z} = require('zod');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converts a DNIO Data Service schema definition into Zod schemas
|
|
8
|
+
* used by MCP tool inputSchema.
|
|
9
|
+
*
|
|
10
|
+
* DNIO schemas are JSON objects with a `definition` array, where each
|
|
11
|
+
* element has: { key, type, properties, definition (for nested) }
|
|
12
|
+
*
|
|
13
|
+
* Supported DNIO types: String, Number, Boolean, Date, Object, Array, Relation (Object)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ─── Primitive converters ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function dnioTypeToZod(field) {
|
|
19
|
+
const type = field.type;
|
|
20
|
+
const props = field.properties || {};
|
|
21
|
+
const desc = props.name || field.key;
|
|
22
|
+
|
|
23
|
+
switch (type) {
|
|
24
|
+
case 'String':
|
|
25
|
+
return z.string().optional().describe(desc);
|
|
26
|
+
|
|
27
|
+
case 'Number': {
|
|
28
|
+
let schema = z.number();
|
|
29
|
+
if (props.precision != null) {
|
|
30
|
+
schema = schema.describe(`${desc} (precision: ${props.precision})`);
|
|
31
|
+
} else {
|
|
32
|
+
schema = schema.describe(desc);
|
|
33
|
+
}
|
|
34
|
+
return schema.optional();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case 'Boolean':
|
|
38
|
+
return z.boolean().optional().describe(desc);
|
|
39
|
+
|
|
40
|
+
case 'Date':
|
|
41
|
+
return z.string().optional().describe(`${desc} (ISO 8601 date string)`);
|
|
42
|
+
|
|
43
|
+
case 'Object':
|
|
44
|
+
return buildObjectSchema(field.definition || []).optional().describe(desc);
|
|
45
|
+
|
|
46
|
+
case 'Array':
|
|
47
|
+
return buildArraySchema(field).optional().describe(desc);
|
|
48
|
+
|
|
49
|
+
default:
|
|
50
|
+
logger.warn(`Unknown DNIO type '${type}' for field '${field.key}', defaulting to string`);
|
|
51
|
+
return z.any().optional().describe(desc);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildObjectSchema(definition) {
|
|
56
|
+
const shape = {};
|
|
57
|
+
for (const field of definition) {
|
|
58
|
+
if (field.key === '_self') continue; // skip array wrapper markers
|
|
59
|
+
shape[field.key] = dnioTypeToZod(field);
|
|
60
|
+
}
|
|
61
|
+
return z.object(shape);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildArraySchema(field) {
|
|
65
|
+
const innerDef = field.definition || [];
|
|
66
|
+
if (innerDef.length === 0) {
|
|
67
|
+
return z.array(z.any());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If the inner type is _self with a primitive, it's a simple array
|
|
71
|
+
const selfField = innerDef.find(f => f.key === '_self');
|
|
72
|
+
if (selfField) {
|
|
73
|
+
if (selfField.type === 'Object' && selfField.definition) {
|
|
74
|
+
// Array of objects
|
|
75
|
+
return z.array(buildObjectSchema(selfField.definition));
|
|
76
|
+
}
|
|
77
|
+
// Array of primitives
|
|
78
|
+
return z.array(dnioTypeToZod({...selfField, key: field.key}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Otherwise it's an array of objects
|
|
82
|
+
return z.array(buildObjectSchema(innerDef));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert a DNIO service definition array into a Zod schema for record creation.
|
|
89
|
+
* Excludes _id (auto-generated) and internal fields.
|
|
90
|
+
*
|
|
91
|
+
* @param {Array} definition - DNIO service `definition` array
|
|
92
|
+
* @returns {z.ZodObject} Zod schema for creating a record
|
|
93
|
+
*/
|
|
94
|
+
function definitionToCreateSchema(definition) {
|
|
95
|
+
const shape = {};
|
|
96
|
+
for (const field of definition) {
|
|
97
|
+
// Skip _id (auto-generated), _metadata, __v, and internal fields
|
|
98
|
+
if (['_id', '_metadata', '__v'].includes(field.key)) continue;
|
|
99
|
+
|
|
100
|
+
// For Relation types (stored as Object with _id, _href), just accept the _id
|
|
101
|
+
if (field.properties?._typeChanged === 'Relation') {
|
|
102
|
+
shape[field.key] = z.object({
|
|
103
|
+
_id: z.string().describe(`${field.properties.name || field.key} - related record ID`)
|
|
104
|
+
}).optional().describe(field.properties.name || field.key);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// For secure/password fields, accept plain string (encryption handled server-side)
|
|
109
|
+
if (field.properties?.password) {
|
|
110
|
+
shape[field.key] = z.string().optional().describe(
|
|
111
|
+
`${field.properties.name || field.key} (secure field)`
|
|
112
|
+
);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
shape[field.key] = dnioTypeToZod(field);
|
|
117
|
+
}
|
|
118
|
+
return z.object(shape);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert a DNIO service definition into a Zod schema for record updates.
|
|
123
|
+
* Same as create but all fields are optional (partial update).
|
|
124
|
+
*
|
|
125
|
+
* @param {Array} definition - DNIO service `definition` array
|
|
126
|
+
* @returns {z.ZodObject} Zod schema for updating a record
|
|
127
|
+
*/
|
|
128
|
+
function definitionToUpdateSchema(definition) {
|
|
129
|
+
return definitionToCreateSchema(definition).partial();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate a human-readable schema description string for LLM context.
|
|
134
|
+
*
|
|
135
|
+
* @param {Array} definition - DNIO service `definition` array
|
|
136
|
+
* @param {number} [indent=0] - indentation depth
|
|
137
|
+
* @returns {string} Formatted schema description
|
|
138
|
+
*/
|
|
139
|
+
function schemaToDescription(definition, indent = 0) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
const pad = ' '.repeat(indent);
|
|
142
|
+
|
|
143
|
+
for (const field of definition) {
|
|
144
|
+
if (['_metadata', '__v'].includes(field.key)) continue;
|
|
145
|
+
|
|
146
|
+
const name = field.properties?.name || field.key;
|
|
147
|
+
const required = field.properties?.required ? ' (required)' : '';
|
|
148
|
+
const type = field.properties?._typeChanged === 'Relation'
|
|
149
|
+
? `Relation → ${field.properties.relatedTo}`
|
|
150
|
+
: field.type;
|
|
151
|
+
|
|
152
|
+
let line = `${pad}- ${field.key}: ${type}${required}`;
|
|
153
|
+
if (name !== field.key) line += ` // ${name}`;
|
|
154
|
+
|
|
155
|
+
lines.push(line);
|
|
156
|
+
|
|
157
|
+
// Recurse into nested objects/arrays
|
|
158
|
+
if (field.definition && field.key !== '_self') {
|
|
159
|
+
const nested = field.definition.filter(f => f.key !== '_self');
|
|
160
|
+
if (nested.length > 0) {
|
|
161
|
+
lines.push(schemaToDescription(nested, indent + 1));
|
|
162
|
+
}
|
|
163
|
+
// For arrays with _self object defs
|
|
164
|
+
const selfObj = field.definition.find(f => f.key === '_self' && f.definition);
|
|
165
|
+
if (selfObj?.definition) {
|
|
166
|
+
lines.push(schemaToDescription(selfObj.definition, indent + 1));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
definitionToCreateSchema,
|
|
175
|
+
definitionToUpdateSchema,
|
|
176
|
+
schemaToDescription,
|
|
177
|
+
dnioTypeToZod,
|
|
178
|
+
buildObjectSchema
|
|
179
|
+
};
|