@matimo/core 0.1.0-alpha.9 → 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 +341 -14
- package/dist/approval/approval-handler.d.ts +5 -1
- package/dist/approval/approval-handler.d.ts.map +1 -1
- package/dist/approval/approval-handler.js +6 -0
- package/dist/approval/approval-handler.js.map +1 -1
- package/dist/core/schema.d.ts +41 -10
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +40 -4
- package/dist/core/schema.js.map +1 -1
- package/dist/core/skill-content-parser.d.ts +91 -0
- package/dist/core/skill-content-parser.d.ts.map +1 -0
- package/dist/core/skill-content-parser.js +248 -0
- package/dist/core/skill-content-parser.js.map +1 -0
- package/dist/core/skill-loader.d.ts +46 -0
- package/dist/core/skill-loader.d.ts.map +1 -0
- package/dist/core/skill-loader.js +310 -0
- package/dist/core/skill-loader.js.map +1 -0
- package/dist/core/skill-registry.d.ts +131 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +316 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/tfidf-embedding.d.ts +45 -0
- package/dist/core/tfidf-embedding.d.ts.map +1 -0
- package/dist/core/tfidf-embedding.js +199 -0
- package/dist/core/tfidf-embedding.js.map +1 -0
- package/dist/core/tool-loader.d.ts +3 -1
- package/dist/core/tool-loader.d.ts.map +1 -1
- package/dist/core/tool-loader.js +33 -10
- package/dist/core/tool-loader.js.map +1 -1
- package/dist/core/types.d.ts +203 -6
- package/dist/core/types.d.ts.map +1 -1
- package/dist/encodings/parameter-encoding.d.ts +1 -1
- package/dist/encodings/parameter-encoding.d.ts.map +1 -1
- package/dist/encodings/parameter-encoding.js +9 -4
- package/dist/encodings/parameter-encoding.js.map +1 -1
- package/dist/errors/matimo-error.d.ts +11 -2
- package/dist/errors/matimo-error.d.ts.map +1 -1
- package/dist/errors/matimo-error.js +25 -1
- package/dist/errors/matimo-error.js.map +1 -1
- package/dist/executors/command-executor.d.ts +9 -2
- package/dist/executors/command-executor.d.ts.map +1 -1
- package/dist/executors/command-executor.js +29 -5
- package/dist/executors/command-executor.js.map +1 -1
- package/dist/executors/function-executor.d.ts +10 -3
- package/dist/executors/function-executor.d.ts.map +1 -1
- package/dist/executors/function-executor.js +44 -24
- package/dist/executors/function-executor.js.map +1 -1
- package/dist/executors/http-executor.d.ts +79 -4
- package/dist/executors/http-executor.d.ts.map +1 -1
- package/dist/executors/http-executor.js +232 -28
- package/dist/executors/http-executor.js.map +1 -1
- package/dist/index.d.ts +25 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/langchain.d.ts +55 -0
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +71 -4
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/logging/winston-logger.d.ts.map +1 -1
- package/dist/logging/winston-logger.js +9 -1
- package/dist/logging/winston-logger.js.map +1 -1
- package/dist/matimo-instance.d.ts +230 -18
- package/dist/matimo-instance.d.ts.map +1 -1
- package/dist/matimo-instance.js +739 -40
- package/dist/matimo-instance.js.map +1 -1
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +141 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +754 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/mcp/secrets/aws-resolver.d.ts +41 -0
- package/dist/mcp/secrets/aws-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/aws-resolver.js +141 -0
- package/dist/mcp/secrets/aws-resolver.js.map +1 -0
- package/dist/mcp/secrets/dotenv-resolver.d.ts +23 -0
- package/dist/mcp/secrets/dotenv-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/dotenv-resolver.js +94 -0
- package/dist/mcp/secrets/dotenv-resolver.js.map +1 -0
- package/dist/mcp/secrets/env-resolver.d.ts +14 -0
- package/dist/mcp/secrets/env-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/env-resolver.js +27 -0
- package/dist/mcp/secrets/env-resolver.js.map +1 -0
- package/dist/mcp/secrets/index.d.ts +14 -0
- package/dist/mcp/secrets/index.d.ts.map +1 -0
- package/dist/mcp/secrets/index.js +13 -0
- package/dist/mcp/secrets/index.js.map +1 -0
- package/dist/mcp/secrets/resolver-chain.d.ts +34 -0
- package/dist/mcp/secrets/resolver-chain.d.ts.map +1 -0
- package/dist/mcp/secrets/resolver-chain.js +141 -0
- package/dist/mcp/secrets/resolver-chain.js.map +1 -0
- package/dist/mcp/secrets/types.d.ts +73 -0
- package/dist/mcp/secrets/types.d.ts.map +1 -0
- package/dist/mcp/secrets/types.js +8 -0
- package/dist/mcp/secrets/types.js.map +1 -0
- package/dist/mcp/secrets/vault-resolver.d.ts +43 -0
- package/dist/mcp/secrets/vault-resolver.d.ts.map +1 -0
- package/dist/mcp/secrets/vault-resolver.js +127 -0
- package/dist/mcp/secrets/vault-resolver.js.map +1 -0
- package/dist/mcp/tool-converter.d.ts +40 -0
- package/dist/mcp/tool-converter.d.ts.map +1 -0
- package/dist/mcp/tool-converter.js +185 -0
- package/dist/mcp/tool-converter.js.map +1 -0
- package/dist/policy/approval-manifest.d.ts +76 -0
- package/dist/policy/approval-manifest.d.ts.map +1 -0
- package/dist/policy/approval-manifest.js +197 -0
- package/dist/policy/approval-manifest.js.map +1 -0
- package/dist/policy/content-validator.d.ts +19 -0
- package/dist/policy/content-validator.d.ts.map +1 -0
- package/dist/policy/content-validator.js +196 -0
- package/dist/policy/content-validator.js.map +1 -0
- package/dist/policy/default-policy.d.ts +46 -0
- package/dist/policy/default-policy.d.ts.map +1 -0
- package/dist/policy/default-policy.js +241 -0
- package/dist/policy/default-policy.js.map +1 -0
- package/dist/policy/events.d.ts +71 -0
- package/dist/policy/events.d.ts.map +1 -0
- package/dist/policy/events.js +8 -0
- package/dist/policy/events.js.map +1 -0
- package/dist/policy/index.d.ts +13 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +9 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/integrity-tracker.d.ts +62 -0
- package/dist/policy/integrity-tracker.d.ts.map +1 -0
- package/dist/policy/integrity-tracker.js +79 -0
- package/dist/policy/integrity-tracker.js.map +1 -0
- package/dist/policy/policy-loader.d.ts +58 -0
- package/dist/policy/policy-loader.d.ts.map +1 -0
- package/dist/policy/policy-loader.js +156 -0
- package/dist/policy/policy-loader.js.map +1 -0
- package/dist/policy/risk-classifier.d.ts +18 -0
- package/dist/policy/risk-classifier.d.ts.map +1 -0
- package/dist/policy/risk-classifier.js +47 -0
- package/dist/policy/risk-classifier.js.map +1 -0
- package/dist/policy/types.d.ts +131 -0
- package/dist/policy/types.d.ts.map +1 -0
- package/dist/policy/types.js +8 -0
- package/dist/policy/types.js.map +1 -0
- package/package.json +22 -6
- package/tools/matimo_approve_tool/definition.yaml +36 -0
- package/tools/matimo_approve_tool/matimo_approve_tool.ts +90 -0
- package/tools/matimo_create_skill/definition.yaml +46 -0
- package/tools/matimo_create_skill/matimo_create_skill.ts +75 -0
- package/tools/matimo_create_tool/definition.yaml +48 -0
- package/tools/matimo_create_tool/matimo_create_tool.ts +137 -0
- package/tools/matimo_get_skill/definition.yaml +60 -0
- package/tools/matimo_get_skill/matimo_get_skill.ts +182 -0
- package/tools/matimo_get_tool/definition.yaml +36 -0
- package/tools/matimo_get_tool/matimo_get_tool.ts +56 -0
- package/tools/matimo_get_tool_status/definition.yaml +42 -0
- package/tools/matimo_get_tool_status/matimo_get_tool_status.ts +101 -0
- package/tools/matimo_list_skills/definition.yaml +52 -0
- package/tools/matimo_list_skills/matimo_list_skills.ts +138 -0
- package/tools/matimo_list_user_tools/definition.yaml +32 -0
- package/tools/matimo_list_user_tools/matimo_list_user_tools.ts +74 -0
- package/tools/matimo_reload_tools/definition.yaml +35 -0
- package/tools/matimo_reload_tools/matimo_reload_tools.ts +29 -0
- package/tools/matimo_search_tools/definition.yaml +32 -0
- package/tools/matimo_search_tools/matimo_search_tools.ts +82 -0
- package/tools/matimo_validate_skill/definition.yaml +43 -0
- package/tools/matimo_validate_skill/matimo_validate_skill.ts +137 -0
- package/tools/matimo_validate_tool/definition.yaml +34 -0
- package/tools/matimo_validate_tool/matimo_validate_tool.ts +168 -0
- package/tools/shared/skill-validation.ts +335 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matimo MCP Server
|
|
3
|
+
*
|
|
4
|
+
* Exposes all Matimo tools via the Model Context Protocol.
|
|
5
|
+
* Supports stdio (local/Claude Desktop) and HTTP (remote/Docker) transports.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* Client → MCP Protocol → MCPServer → MatimoInstance.execute() → Tool APIs
|
|
9
|
+
* Secrets resolved via SecretResolverChain (env, dotenv, Vault, AWS SM)
|
|
10
|
+
* HTTP mode protected by Bearer token (MATIMO_MCP_TOKEN)
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { createRequire } from 'module';
|
|
16
|
+
import { MatimoInstance } from '../matimo-instance';
|
|
17
|
+
import { MatimoError, ErrorCode } from '../errors/matimo-error';
|
|
18
|
+
import { getGlobalMatimoLogger, setGlobalMatimoLogger } from '../logging';
|
|
19
|
+
import { createLogger } from '../logging/winston-logger';
|
|
20
|
+
import { toolToMcpRegistration, extractAuthPlaceholders } from './tool-converter';
|
|
21
|
+
import { createResolverChain } from './secrets/resolver-chain';
|
|
22
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
23
|
+
function getPackageVersion() {
|
|
24
|
+
try {
|
|
25
|
+
// Method 1: Use createRequire to find @matimo/core's package.json (works in all environments)
|
|
26
|
+
try {
|
|
27
|
+
const req = typeof require !== 'undefined'
|
|
28
|
+
? require
|
|
29
|
+
: // ESM fallback (eval suppresses type errors)
|
|
30
|
+
createRequire(eval('import.meta.url'));
|
|
31
|
+
const pkgPath = req.resolve('@matimo/core/package.json');
|
|
32
|
+
/* istanbul ignore next -- only reachable when @matimo/core/package.json is a proper export */
|
|
33
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
34
|
+
/* istanbul ignore next */
|
|
35
|
+
return pkg.version;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Fall through to directory walking
|
|
39
|
+
}
|
|
40
|
+
// Method 2: Walk up from current file location
|
|
41
|
+
let currentDir;
|
|
42
|
+
if (typeof __dirname !== 'undefined') {
|
|
43
|
+
currentDir = __dirname;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// istanbul ignore next -- ESM path, unreachable in CJS/Jest
|
|
47
|
+
try {
|
|
48
|
+
const metaUrl = eval('import.meta.url');
|
|
49
|
+
currentDir = dirname(fileURLToPath(metaUrl));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
currentDir = process.cwd();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let dir = currentDir;
|
|
56
|
+
for (let i = 0; i < 6; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
|
|
59
|
+
if (pkg.name === '@matimo/core')
|
|
60
|
+
return pkg.version;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Continue walking up
|
|
64
|
+
}
|
|
65
|
+
dir = dirname(dir);
|
|
66
|
+
}
|
|
67
|
+
// Method 3: Fallback to well-known paths from cwd
|
|
68
|
+
// istanbul ignore next -- only reachable if both require.resolve and directory walking fail
|
|
69
|
+
const fallbackPaths = [
|
|
70
|
+
join(process.cwd(), 'packages', 'core', 'package.json'),
|
|
71
|
+
join(process.cwd(), 'node_modules', '@matimo', 'core', 'package.json'),
|
|
72
|
+
];
|
|
73
|
+
for (const p of fallbackPaths) {
|
|
74
|
+
try {
|
|
75
|
+
const pkg = JSON.parse(readFileSync(p, 'utf-8'));
|
|
76
|
+
if (pkg.name === '@matimo/core')
|
|
77
|
+
return pkg.version;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Continue
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Fallback
|
|
86
|
+
}
|
|
87
|
+
return '0.0.0';
|
|
88
|
+
}
|
|
89
|
+
// ─── MCPServer ──────────────────────────────────────────────────────────
|
|
90
|
+
export class MCPServer {
|
|
91
|
+
constructor(options = {}) {
|
|
92
|
+
this.matimo = null;
|
|
93
|
+
this.resolverChain = null;
|
|
94
|
+
this.mcpServer = null;
|
|
95
|
+
this.httpServer = null;
|
|
96
|
+
/** Filtered tools available for MCP registration */
|
|
97
|
+
this.filteredTools = [];
|
|
98
|
+
/** The active bearer token (explicit, env, or auto-generated) */
|
|
99
|
+
this.activeToken = null;
|
|
100
|
+
/** Registered skill resources, keyed by skill name, for lifecycle management */
|
|
101
|
+
this.registeredSkillResources = new Map();
|
|
102
|
+
/** Resolved auth secrets held in memory — never written to process.env */
|
|
103
|
+
this.resolvedSecrets = {};
|
|
104
|
+
this.options = {
|
|
105
|
+
transport: options.transport ?? 'stdio',
|
|
106
|
+
port: options.port ?? 3000,
|
|
107
|
+
autoDiscover: options.autoDiscover ?? true,
|
|
108
|
+
https: options.https ?? false,
|
|
109
|
+
selfSigned: options.selfSigned ?? false,
|
|
110
|
+
...options,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Get the active bearer token (available after start()) */
|
|
114
|
+
getActiveToken() {
|
|
115
|
+
return this.activeToken;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Hot-reload tools via MatimoInstance and re-register them on the MCP server.
|
|
119
|
+
* In stdio mode, calls sendToolListChanged() to notify the connected client.
|
|
120
|
+
*/
|
|
121
|
+
async reloadTools() {
|
|
122
|
+
if (!this.matimo)
|
|
123
|
+
return;
|
|
124
|
+
const logger = getGlobalMatimoLogger();
|
|
125
|
+
const result = await this.matimo.reloadTools();
|
|
126
|
+
logger.info('MCPServer: tools reloaded via MatimoInstance', {
|
|
127
|
+
loaded: result.loaded,
|
|
128
|
+
removed: result.removed,
|
|
129
|
+
rejected: result.rejected.length,
|
|
130
|
+
});
|
|
131
|
+
// Re-filter tools applying allowSet/denySet
|
|
132
|
+
let tools = this.matimo.listTools();
|
|
133
|
+
if (this.options.tools && this.options.tools.length > 0) {
|
|
134
|
+
const allowSet = new Set(this.options.tools);
|
|
135
|
+
tools = tools.filter((t) => allowSet.has(t.name));
|
|
136
|
+
}
|
|
137
|
+
if (this.options.excludeTools && this.options.excludeTools.length > 0) {
|
|
138
|
+
const denySet = new Set(this.options.excludeTools);
|
|
139
|
+
tools = tools.filter((t) => !denySet.has(t.name));
|
|
140
|
+
}
|
|
141
|
+
this.filteredTools = tools;
|
|
142
|
+
// Notify MCP clients of tool list change (stdio only — single server)
|
|
143
|
+
if (this.mcpServer &&
|
|
144
|
+
typeof this.mcpServer.sendToolListChanged === 'function') {
|
|
145
|
+
this.mcpServer.sendToolListChanged();
|
|
146
|
+
logger.debug('Notified MCP client of tool list change');
|
|
147
|
+
}
|
|
148
|
+
// Re-sync skill resources on the stdio server (remove stale, add new)
|
|
149
|
+
if (this.mcpServer && this.matimo) {
|
|
150
|
+
this.registerSkillResources(this.mcpServer, this.matimo, logger);
|
|
151
|
+
if (typeof this.mcpServer.sendResourceListChanged === 'function') {
|
|
152
|
+
this.mcpServer.sendResourceListChanged();
|
|
153
|
+
logger.debug('Notified MCP client of resource list change');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Start the MCP server.
|
|
159
|
+
*
|
|
160
|
+
* 1. Initialize secret resolver chain
|
|
161
|
+
* 2. Seed process.env with resolved secrets (for MatimoInstance compatibility)
|
|
162
|
+
* 3. Initialize MatimoInstance with tools
|
|
163
|
+
* 4. Register all tools on the MCP server
|
|
164
|
+
* 5. Connect transport (stdio or HTTP)
|
|
165
|
+
*/
|
|
166
|
+
async start() {
|
|
167
|
+
// Step 0: Suppress logging in stdio mode (JSON-RPC protocol requires clean stdout/stderr)
|
|
168
|
+
if (this.options.transport === 'stdio') {
|
|
169
|
+
setGlobalMatimoLogger(createLogger({ logLevel: 'silent', logFormat: 'simple' }));
|
|
170
|
+
}
|
|
171
|
+
const logger = getGlobalMatimoLogger();
|
|
172
|
+
// Step 1: Build secret resolver chain
|
|
173
|
+
this.resolverChain = createResolverChain(this.options.secretResolver);
|
|
174
|
+
logger.info('Secret resolver chain initialized', {
|
|
175
|
+
resolvers: this.resolverChain.getResolvers().map((r) => r.name),
|
|
176
|
+
});
|
|
177
|
+
// Step 1b: Eagerly load dotenv into process.env so that server config
|
|
178
|
+
// values like MATIMO_MCP_TOKEN are available early (before tool registration).
|
|
179
|
+
// seedEnvironmentSecrets() only resolves tool-specific auth placeholders,
|
|
180
|
+
// so without this, .env-only config like MATIMO_MCP_TOKEN would be missed.
|
|
181
|
+
await this.resolverChain.seedProcessEnv();
|
|
182
|
+
// Step 2: Initialize Matimo
|
|
183
|
+
// Pass logLevel: 'silent' in stdio mode to prevent MatimoInstance from
|
|
184
|
+
// creating a non-silent logger that writes to stdout (corrupts JSON-RPC)
|
|
185
|
+
this.matimo = await MatimoInstance.init({
|
|
186
|
+
toolPaths: this.options.toolPaths,
|
|
187
|
+
skillPaths: this.options.skillPaths,
|
|
188
|
+
autoDiscover: this.options.autoDiscover,
|
|
189
|
+
...(this.options.transport === 'stdio' ? { logLevel: 'silent' } : {}),
|
|
190
|
+
...(this.options.policyConfig ? { policyConfig: this.options.policyConfig } : {}),
|
|
191
|
+
...(this.options.untrustedPaths ? { untrustedPaths: this.options.untrustedPaths } : {}),
|
|
192
|
+
...(this.options.approvalSecret ? { approvalSecret: this.options.approvalSecret } : {}),
|
|
193
|
+
...(this.options.approvalDir ? { approvalDir: this.options.approvalDir } : {}),
|
|
194
|
+
});
|
|
195
|
+
// Re-set silent logger after init (MatimoInstance.init overwrites the global logger)
|
|
196
|
+
if (this.options.transport === 'stdio') {
|
|
197
|
+
setGlobalMatimoLogger(createLogger({ logLevel: 'silent', logFormat: 'simple' }));
|
|
198
|
+
}
|
|
199
|
+
// Step 3: Filter tools
|
|
200
|
+
let tools = this.matimo.listTools();
|
|
201
|
+
logger.debug(`MatimoInstance loaded ${tools.length} tools`);
|
|
202
|
+
if (this.options.tools && this.options.tools.length > 0) {
|
|
203
|
+
const allowSet = new Set(this.options.tools);
|
|
204
|
+
tools = tools.filter((t) => allowSet.has(t.name));
|
|
205
|
+
}
|
|
206
|
+
if (this.options.excludeTools && this.options.excludeTools.length > 0) {
|
|
207
|
+
const denySet = new Set(this.options.excludeTools);
|
|
208
|
+
tools = tools.filter((t) => !denySet.has(t.name));
|
|
209
|
+
}
|
|
210
|
+
if (tools.length === 0) {
|
|
211
|
+
logger.warn('No tools available after filtering. MCP server will have zero tools.');
|
|
212
|
+
}
|
|
213
|
+
// Step 4: Resolve auth placeholders for all tools and seed process.env
|
|
214
|
+
await this.seedEnvironmentSecrets(tools);
|
|
215
|
+
// Step 5: Store filtered tools for MCP server creation
|
|
216
|
+
this.filteredTools = tools;
|
|
217
|
+
// Step 6: Connect transport
|
|
218
|
+
// Each transport method creates its own McpServer instance(s).
|
|
219
|
+
// HTTP mode creates a new McpServer per session to support multiple concurrent clients.
|
|
220
|
+
if (this.options.transport === 'stdio') {
|
|
221
|
+
await this.connectStdio();
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
await this.connectHttp();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolve all auth-related secrets for tools and seed them into process.env.
|
|
229
|
+
* This ensures MatimoInstance.injectAuthParameters() can find them.
|
|
230
|
+
*/
|
|
231
|
+
async seedEnvironmentSecrets(tools) {
|
|
232
|
+
if (!this.resolverChain)
|
|
233
|
+
return;
|
|
234
|
+
const logger = getGlobalMatimoLogger();
|
|
235
|
+
// Collect all unique auth placeholders across all tools
|
|
236
|
+
const allPlaceholders = new Set();
|
|
237
|
+
for (const tool of tools) {
|
|
238
|
+
const placeholders = extractAuthPlaceholders(tool);
|
|
239
|
+
for (const p of placeholders) {
|
|
240
|
+
allPlaceholders.add(p);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (allPlaceholders.size === 0)
|
|
244
|
+
return;
|
|
245
|
+
logger.debug('Resolving auth secrets for tools', {
|
|
246
|
+
placeholderCount: allPlaceholders.size,
|
|
247
|
+
});
|
|
248
|
+
// Resolve all at once (efficient for batch-capable resolvers)
|
|
249
|
+
const resolved = await this.resolverChain.resolveAll([...allPlaceholders]);
|
|
250
|
+
// Store resolved secrets in memory only — never write to process.env.
|
|
251
|
+
// Secrets are injected as per-call credentials to matimo.execute() instead,
|
|
252
|
+
// preventing accidental leakage into child processes spawned by other code.
|
|
253
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
254
|
+
this.resolvedSecrets[key] = value;
|
|
255
|
+
this.resolvedSecrets[`MATIMO_${key}`] = value;
|
|
256
|
+
}
|
|
257
|
+
logger.debug('Auth secrets resolved and stored in memory (not in process.env)', {
|
|
258
|
+
resolved: Object.keys(resolved).length,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Register (or re-register) all skills from `matimo` as MCP resources on `server`.
|
|
263
|
+
* Removes any previously registered skill resources before adding the current set
|
|
264
|
+
* so that this method is safe to call multiple times (e.g., from reloadTools).
|
|
265
|
+
*/
|
|
266
|
+
registerSkillResources(server, matimo, logger) {
|
|
267
|
+
// Remove stale skill resources registered from a previous call
|
|
268
|
+
for (const [, handle] of this.registeredSkillResources) {
|
|
269
|
+
try {
|
|
270
|
+
handle.remove();
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Ignore — resource may already have been removed
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
this.registeredSkillResources.clear();
|
|
277
|
+
const skills = matimo.listSkills();
|
|
278
|
+
let registered = 0;
|
|
279
|
+
for (const skill of skills) {
|
|
280
|
+
try {
|
|
281
|
+
const handle = server.registerResource(skill.name, `skills://${skill.name}`, {
|
|
282
|
+
title: skill.name,
|
|
283
|
+
description: skill.description,
|
|
284
|
+
mimeType: 'text/markdown',
|
|
285
|
+
}, async () => {
|
|
286
|
+
const content = matimo.getSkillContent(skill.name);
|
|
287
|
+
return {
|
|
288
|
+
contents: [
|
|
289
|
+
{
|
|
290
|
+
uri: `skills://${skill.name}`,
|
|
291
|
+
text: content ?? `Skill "${skill.name}" content unavailable`,
|
|
292
|
+
mimeType: 'text/markdown',
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
this.registeredSkillResources.set(skill.name, handle);
|
|
298
|
+
registered++;
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
logger.debug(`Failed to register skill resource '${skill.name}'`, {
|
|
302
|
+
skillName: skill.name,
|
|
303
|
+
error: err instanceof Error ? err.message : String(err),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (registered > 0) {
|
|
308
|
+
logger.debug(`Registered ${registered} skill resources on MCP server`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Create a new McpServer instance with all filtered tools registered.
|
|
313
|
+
* Each call returns a fresh server — used per-session in HTTP mode.
|
|
314
|
+
*/
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
316
|
+
async createMcpServerWithTools() {
|
|
317
|
+
const { McpServer } = await import(
|
|
318
|
+
// @ts-ignore — TS2307: module not found at compile time, resolves at runtime
|
|
319
|
+
'@modelcontextprotocol/sdk/server/mcp');
|
|
320
|
+
const logger = getGlobalMatimoLogger();
|
|
321
|
+
const matimo = this.matimo;
|
|
322
|
+
const version = getPackageVersion();
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
|
+
const server = new McpServer({ name: 'matimo', version });
|
|
325
|
+
let registeredCount = 0;
|
|
326
|
+
for (const tool of this.filteredTools) {
|
|
327
|
+
try {
|
|
328
|
+
const registration = toolToMcpRegistration(tool);
|
|
329
|
+
server.registerTool(tool.name, {
|
|
330
|
+
title: registration.title,
|
|
331
|
+
description: registration.description,
|
|
332
|
+
inputSchema: registration.inputSchema,
|
|
333
|
+
}, async (args) => {
|
|
334
|
+
try {
|
|
335
|
+
logger.debug(`MCP tool call: ${tool.name}`, {
|
|
336
|
+
toolName: tool.name,
|
|
337
|
+
argCount: Object.keys(args).length,
|
|
338
|
+
});
|
|
339
|
+
if (tool.requires_approval) {
|
|
340
|
+
const approved = args._matimo_approved;
|
|
341
|
+
if (approved !== true) {
|
|
342
|
+
throw new MatimoError(`Tool '${tool.name}' requires approval. This is a destructive operation. Re-invoke with parameter _matimo_approved: true to confirm execution.`, ErrorCode.EXECUTION_FAILED);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Strip _matimo_approved from args before passing to execute.
|
|
346
|
+
// By default this client-supplied flag is only a confirmation
|
|
347
|
+
// prompt signal; it must not bypass server-side approval checks.
|
|
348
|
+
const { _matimo_approved, ...cleanArgs } = args;
|
|
349
|
+
const result = await matimo.execute(tool.name, cleanArgs, {
|
|
350
|
+
approved: this.options.trustClientApproval === true &&
|
|
351
|
+
tool.requires_approval === true &&
|
|
352
|
+
_matimo_approved === true,
|
|
353
|
+
credentials: this.resolvedSecrets,
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
content: [
|
|
357
|
+
{
|
|
358
|
+
type: 'text',
|
|
359
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
logger.error(`MCP tool call failed: ${tool.name}`, {
|
|
366
|
+
toolName: tool.name,
|
|
367
|
+
error: error instanceof Error ? error.message : String(error),
|
|
368
|
+
});
|
|
369
|
+
return {
|
|
370
|
+
content: [
|
|
371
|
+
{
|
|
372
|
+
type: 'text',
|
|
373
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
isError: true,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
registeredCount++;
|
|
381
|
+
}
|
|
382
|
+
catch (regError) {
|
|
383
|
+
logger.error(`Failed to register tool '${tool.name}'`, {
|
|
384
|
+
toolName: tool.name,
|
|
385
|
+
error: regError instanceof Error ? regError.message : String(regError),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
logger.debug(`Registered ${registeredCount}/${this.filteredTools.length} tools on MCP server`);
|
|
390
|
+
// Register skills as MCP resources (skills://name)
|
|
391
|
+
// This allows agents (Claude Desktop, Cursor, etc.) to attach skills directly
|
|
392
|
+
// from the resource picker — no tool calls needed.
|
|
393
|
+
this.registerSkillResources(server, matimo, logger);
|
|
394
|
+
return server;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Connect via stdio transport (for Claude Desktop, Cursor, etc.)
|
|
398
|
+
*/
|
|
399
|
+
async connectStdio() {
|
|
400
|
+
const { StdioServerTransport } = await import(
|
|
401
|
+
// @ts-ignore — TS2307: module not found at compile time, resolves at runtime
|
|
402
|
+
'@modelcontextprotocol/sdk/server/stdio');
|
|
403
|
+
const server = await this.createMcpServerWithTools();
|
|
404
|
+
this.mcpServer = server;
|
|
405
|
+
const transport = new StdioServerTransport();
|
|
406
|
+
await server.connect(transport);
|
|
407
|
+
const logger = getGlobalMatimoLogger();
|
|
408
|
+
logger.info('Matimo MCP server started (stdio)', {
|
|
409
|
+
transport: 'stdio',
|
|
410
|
+
tools: this.filteredTools.length,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Connect via HTTP/HTTPS transport with Bearer token auth.
|
|
415
|
+
* Creates a new McpServer + transport per session to support multiple concurrent clients.
|
|
416
|
+
* Auto-generates a bearer token if none is provided.
|
|
417
|
+
*/
|
|
418
|
+
async connectHttp() {
|
|
419
|
+
const http = await import('http');
|
|
420
|
+
const { StreamableHTTPServerTransport } = (await import(
|
|
421
|
+
// @ts-ignore — TS2307: module not found at compile time, resolves at runtime
|
|
422
|
+
'@modelcontextprotocol/sdk/server/streamableHttp'
|
|
423
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
424
|
+
));
|
|
425
|
+
const { randomUUID } = await import('crypto');
|
|
426
|
+
const { isInitializeRequest } = (await import(
|
|
427
|
+
// @ts-ignore — TS2307: module not found at compile time, resolves at runtime
|
|
428
|
+
'@modelcontextprotocol/sdk/types'
|
|
429
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
430
|
+
));
|
|
431
|
+
const logger = getGlobalMatimoLogger();
|
|
432
|
+
// Resolve bearer token: explicit > env > auto-generate
|
|
433
|
+
let mcpToken = this.options.mcpToken ?? process.env.MATIMO_MCP_TOKEN;
|
|
434
|
+
let tokenAutoGenerated = false;
|
|
435
|
+
if (!mcpToken) {
|
|
436
|
+
mcpToken = randomUUID();
|
|
437
|
+
tokenAutoGenerated = true;
|
|
438
|
+
logger.info('Auto-generated bearer token for HTTP mode');
|
|
439
|
+
}
|
|
440
|
+
this.activeToken = mcpToken;
|
|
441
|
+
// Determine protocol (HTTP vs HTTPS)
|
|
442
|
+
const useHttps = this.options.https || this.options.selfSigned || !!this.options.certPath;
|
|
443
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
444
|
+
// Track active sessions: sessionId → { transport, server }
|
|
445
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
446
|
+
const sessions = new Map();
|
|
447
|
+
const requestHandler = async (req, res) => {
|
|
448
|
+
// CORS headers
|
|
449
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
450
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
451
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id');
|
|
452
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
453
|
+
// Handle CORS preflight
|
|
454
|
+
if (req.method === 'OPTIONS') {
|
|
455
|
+
res.writeHead(204);
|
|
456
|
+
res.end();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
// Health check (no auth required)
|
|
460
|
+
if (req.url === '/health') {
|
|
461
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
462
|
+
res.end(JSON.stringify({ status: 'ok', tools: this.filteredTools.length }));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Bearer token auth (always enabled in HTTP mode)
|
|
466
|
+
const authHeader = req.headers.authorization;
|
|
467
|
+
if (!authHeader || authHeader !== `Bearer ${mcpToken}`) {
|
|
468
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
469
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// MCP endpoint
|
|
473
|
+
if (req.url === '/mcp' || req.url === '/') {
|
|
474
|
+
// Parse request body for POST requests
|
|
475
|
+
let body;
|
|
476
|
+
if (req.method === 'POST') {
|
|
477
|
+
try {
|
|
478
|
+
body = await new Promise((resolve, reject) => {
|
|
479
|
+
let data = '';
|
|
480
|
+
req.on('data', (chunk) => {
|
|
481
|
+
data += chunk;
|
|
482
|
+
});
|
|
483
|
+
req.on('end', () => {
|
|
484
|
+
try {
|
|
485
|
+
resolve(JSON.parse(data));
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
reject(new Error('Invalid JSON'));
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
req.on('error', reject);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (parseErr) {
|
|
495
|
+
const message = parseErr instanceof Error ? parseErr.message : 'Invalid request body';
|
|
496
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
497
|
+
res.end(JSON.stringify({
|
|
498
|
+
jsonrpc: '2.0',
|
|
499
|
+
error: { code: -32700, message },
|
|
500
|
+
id: null,
|
|
501
|
+
}));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
506
|
+
// Route to existing session
|
|
507
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
508
|
+
const session = sessions.get(sessionId);
|
|
509
|
+
await session.transport.handleRequest(req, res, body);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// DELETE for unknown session
|
|
513
|
+
if (req.method === 'DELETE') {
|
|
514
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
515
|
+
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// GET for SSE stream without session
|
|
519
|
+
if (req.method === 'GET') {
|
|
520
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
521
|
+
res.end(JSON.stringify({ error: 'Invalid or missing session ID' }));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// Only create new session for initialization requests (official MCP pattern)
|
|
525
|
+
if (!isInitializeRequest(body)) {
|
|
526
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
527
|
+
res.end(JSON.stringify({
|
|
528
|
+
jsonrpc: '2.0',
|
|
529
|
+
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
|
|
530
|
+
id: null,
|
|
531
|
+
}));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// New session: create a fresh McpServer + transport.
|
|
535
|
+
// Declare mcpServer before constructing the transport so the onsessioninitialized
|
|
536
|
+
// closure captures the variable reference rather than an uninitialized binding (TDZ).
|
|
537
|
+
// eslint-disable-next-line prefer-const
|
|
538
|
+
let mcpServer;
|
|
539
|
+
const transport = new StreamableHTTPServerTransport({
|
|
540
|
+
sessionIdGenerator: () => randomUUID(),
|
|
541
|
+
onsessioninitialized: (sid) => {
|
|
542
|
+
sessions.set(sid, { transport, server: mcpServer });
|
|
543
|
+
logger.debug(`New MCP session: ${sid}`);
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
// Clean up session when transport closes
|
|
547
|
+
transport.onclose = () => {
|
|
548
|
+
const sid = transport.sessionId;
|
|
549
|
+
if (sid && sessions.has(sid)) {
|
|
550
|
+
sessions.delete(sid);
|
|
551
|
+
logger.debug(`MCP session closed: ${sid}`);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
mcpServer = await this.createMcpServerWithTools();
|
|
555
|
+
await mcpServer.connect(transport);
|
|
556
|
+
// Handle the initialization request
|
|
557
|
+
await transport.handleRequest(req, res, body);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
561
|
+
res.end(JSON.stringify({ error: 'Not found. Use /mcp for MCP protocol.' }));
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
// Create HTTP or HTTPS server
|
|
565
|
+
let httpServer;
|
|
566
|
+
if (useHttps) {
|
|
567
|
+
const tlsOptions = await this.getTlsOptions();
|
|
568
|
+
const https = await import('https');
|
|
569
|
+
httpServer = https.createServer(tlsOptions, requestHandler);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
httpServer = http.createServer(requestHandler);
|
|
573
|
+
}
|
|
574
|
+
this.httpServer = httpServer;
|
|
575
|
+
const url = `${protocol}://localhost:${this.options.port}/mcp`;
|
|
576
|
+
await new Promise((resolve) => {
|
|
577
|
+
httpServer.listen(this.options.port, () => {
|
|
578
|
+
logger.info(`Matimo MCP server started (${protocol.toUpperCase()})`, {
|
|
579
|
+
transport: protocol,
|
|
580
|
+
port: this.options.port,
|
|
581
|
+
tools: this.filteredTools.length,
|
|
582
|
+
authenticated: true,
|
|
583
|
+
tokenAutoGenerated,
|
|
584
|
+
url,
|
|
585
|
+
});
|
|
586
|
+
resolve();
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get TLS options for HTTPS mode.
|
|
592
|
+
* Supports user-provided certs or auto-generated self-signed certs.
|
|
593
|
+
*/
|
|
594
|
+
async getTlsOptions() {
|
|
595
|
+
// User-provided certificates
|
|
596
|
+
if (this.options.certPath && this.options.keyPath) {
|
|
597
|
+
if (!existsSync(this.options.certPath)) {
|
|
598
|
+
throw new MatimoError(`TLS certificate not found: ${this.options.certPath}`, ErrorCode.EXECUTION_FAILED);
|
|
599
|
+
}
|
|
600
|
+
if (!existsSync(this.options.keyPath)) {
|
|
601
|
+
throw new MatimoError(`TLS private key not found: ${this.options.keyPath}`, ErrorCode.EXECUTION_FAILED);
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
cert: readFileSync(this.options.certPath, 'utf-8'),
|
|
605
|
+
key: readFileSync(this.options.keyPath, 'utf-8'),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
// Self-signed certificate generation
|
|
609
|
+
return this.generateSelfSignedCert();
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Generate a self-signed TLS certificate using Node.js crypto.
|
|
613
|
+
* Certs are cached in .matimo/certs/ for reuse across restarts.
|
|
614
|
+
*/
|
|
615
|
+
async generateSelfSignedCert() {
|
|
616
|
+
const certsDir = join(process.cwd(), '.matimo', 'certs');
|
|
617
|
+
const certFile = join(certsDir, 'server.crt');
|
|
618
|
+
const keyFile = join(certsDir, 'server.key');
|
|
619
|
+
// Return cached certs if they exist
|
|
620
|
+
if (existsSync(certFile) && existsSync(keyFile)) {
|
|
621
|
+
const logger = getGlobalMatimoLogger();
|
|
622
|
+
logger.info('Using cached self-signed certificate from .matimo/certs/');
|
|
623
|
+
return {
|
|
624
|
+
cert: readFileSync(certFile, 'utf-8'),
|
|
625
|
+
key: readFileSync(keyFile, 'utf-8'),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
// Generate new self-signed cert using Node.js crypto
|
|
629
|
+
const { generateKeyPairSync } = await import('crypto');
|
|
630
|
+
const logger = getGlobalMatimoLogger();
|
|
631
|
+
logger.info('Generating self-signed TLS certificate...');
|
|
632
|
+
// Generate RSA key pair
|
|
633
|
+
const { privateKey } = generateKeyPairSync('rsa', {
|
|
634
|
+
modulusLength: 2048,
|
|
635
|
+
});
|
|
636
|
+
// Build self-signed X.509 certificate using forge-free ASN.1
|
|
637
|
+
// For simplicity, use openssl via child_process if available, otherwise fallback
|
|
638
|
+
const keyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
639
|
+
const certPem = await this.createSelfSignedCertViaCli(keyPem);
|
|
640
|
+
// Save to .matimo/certs/
|
|
641
|
+
mkdirSync(certsDir, { recursive: true });
|
|
642
|
+
writeFileSync(certFile, certPem, { mode: 0o644 });
|
|
643
|
+
writeFileSync(keyFile, keyPem, { mode: 0o600 });
|
|
644
|
+
logger.info('Self-signed certificate saved to .matimo/certs/');
|
|
645
|
+
return { cert: certPem, key: keyPem };
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Create a self-signed certificate using openssl CLI.
|
|
649
|
+
* Throws if openssl is unavailable or fails — provide --cert and --key paths as an alternative.
|
|
650
|
+
*/
|
|
651
|
+
// istanbul ignore next -- requires openssl CLI; covered by integration tests
|
|
652
|
+
async createSelfSignedCertViaCli(keyPem) {
|
|
653
|
+
const { execFileSync } = await import('child_process');
|
|
654
|
+
const { tmpdir } = await import('os');
|
|
655
|
+
const { randomBytes } = await import('crypto');
|
|
656
|
+
const tmpKey = join(tmpdir(), `matimo-key-${randomBytes(4).toString('hex')}.pem`);
|
|
657
|
+
const tmpCert = join(tmpdir(), `matimo-cert-${randomBytes(4).toString('hex')}.pem`);
|
|
658
|
+
try {
|
|
659
|
+
writeFileSync(tmpKey, keyPem, { mode: 0o600 });
|
|
660
|
+
// Use execFileSync with an args array to avoid shell-specific redirection (e.g. 2>/dev/null)
|
|
661
|
+
// which is POSIX-only. stderr is suppressed via stdio: 'pipe'.
|
|
662
|
+
execFileSync('openssl', [
|
|
663
|
+
'req',
|
|
664
|
+
'-new',
|
|
665
|
+
'-x509',
|
|
666
|
+
'-key',
|
|
667
|
+
tmpKey,
|
|
668
|
+
'-out',
|
|
669
|
+
tmpCert,
|
|
670
|
+
'-days',
|
|
671
|
+
'365',
|
|
672
|
+
'-subj',
|
|
673
|
+
'/CN=localhost/O=Matimo MCP Server',
|
|
674
|
+
'-addext',
|
|
675
|
+
'subjectAltName=DNS:localhost,IP:127.0.0.1',
|
|
676
|
+
], { stdio: 'pipe' });
|
|
677
|
+
const cert = readFileSync(tmpCert, 'utf-8');
|
|
678
|
+
return cert;
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
682
|
+
throw new MatimoError(`Failed to generate self-signed certificate: ${reason}. Install openssl or provide --cert and --key paths.`, ErrorCode.EXECUTION_FAILED);
|
|
683
|
+
}
|
|
684
|
+
finally {
|
|
685
|
+
// Clean up temp files
|
|
686
|
+
try {
|
|
687
|
+
const { unlinkSync } = await import('fs');
|
|
688
|
+
unlinkSync(tmpKey);
|
|
689
|
+
unlinkSync(tmpCert);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Ignore cleanup errors
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Gracefully stop the MCP server.
|
|
698
|
+
*/
|
|
699
|
+
async stop() {
|
|
700
|
+
const logger = getGlobalMatimoLogger();
|
|
701
|
+
// Close MCP server
|
|
702
|
+
if (this.mcpServer) {
|
|
703
|
+
const server = this.mcpServer;
|
|
704
|
+
if (server.close) {
|
|
705
|
+
await server.close();
|
|
706
|
+
}
|
|
707
|
+
this.mcpServer = null;
|
|
708
|
+
}
|
|
709
|
+
// Close HTTP server.
|
|
710
|
+
// Proactively drain keep-alive and SSE connections so close() can complete.
|
|
711
|
+
// closeIdleConnections() (Node ≥ 18.2) ends idle keep-alive sockets; if active
|
|
712
|
+
// SSE streams are still open, closeAllConnections() (Node ≥ 18.2) forces them
|
|
713
|
+
// closed so the callback is guaranteed to fire.
|
|
714
|
+
if (this.httpServer) {
|
|
715
|
+
const server = this.httpServer;
|
|
716
|
+
if (typeof server.closeIdleConnections === 'function') {
|
|
717
|
+
server.closeIdleConnections();
|
|
718
|
+
}
|
|
719
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
720
|
+
server.closeAllConnections();
|
|
721
|
+
}
|
|
722
|
+
await new Promise((resolve, reject) => {
|
|
723
|
+
server.close((err) => {
|
|
724
|
+
if (err) {
|
|
725
|
+
return reject(err);
|
|
726
|
+
}
|
|
727
|
+
resolve();
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
this.httpServer = null;
|
|
731
|
+
}
|
|
732
|
+
// Dispose secret resolvers (flush caches, close connections)
|
|
733
|
+
if (this.resolverChain) {
|
|
734
|
+
await this.resolverChain.dispose();
|
|
735
|
+
this.resolverChain = null;
|
|
736
|
+
}
|
|
737
|
+
this.matimo = null;
|
|
738
|
+
logger.info('Matimo MCP server stopped');
|
|
739
|
+
}
|
|
740
|
+
/** Get the MatimoInstance (for testing) */
|
|
741
|
+
getMatimoInstance() {
|
|
742
|
+
return this.matimo;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Factory function to create and start an MCP server.
|
|
747
|
+
* Convenience for one-liner usage.
|
|
748
|
+
*/
|
|
749
|
+
export async function createMCPServer(options) {
|
|
750
|
+
const server = new MCPServer(options);
|
|
751
|
+
await server.start();
|
|
752
|
+
return server;
|
|
753
|
+
}
|
|
754
|
+
//# sourceMappingURL=mcp-server.js.map
|