@nogataka/smart-edit 0.0.14
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/LICENSE +22 -0
- package/README.md +244 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/devtools/generate_prompt_factory.d.ts +5 -0
- package/dist/devtools/generate_prompt_factory.js +114 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +34 -0
- package/dist/interprompt/index.d.ts +2 -0
- package/dist/interprompt/index.js +1 -0
- package/dist/interprompt/jinja_template.d.ts +10 -0
- package/dist/interprompt/jinja_template.js +174 -0
- package/dist/interprompt/multilang_prompt.d.ts +54 -0
- package/dist/interprompt/multilang_prompt.js +302 -0
- package/dist/interprompt/prompt_factory.d.ts +16 -0
- package/dist/interprompt/prompt_factory.js +189 -0
- package/dist/interprompt/util/class_decorators.d.ts +1 -0
- package/dist/interprompt/util/class_decorators.js +1 -0
- package/dist/interprompt/util/index.d.ts +1 -0
- package/dist/interprompt/util/index.js +1 -0
- package/dist/serena/agent.d.ts +118 -0
- package/dist/serena/agent.js +675 -0
- package/dist/serena/agno.d.ts +111 -0
- package/dist/serena/agno.js +278 -0
- package/dist/serena/analytics.d.ts +24 -0
- package/dist/serena/analytics.js +119 -0
- package/dist/serena/cli.d.ts +9 -0
- package/dist/serena/cli.js +731 -0
- package/dist/serena/code_editor.d.ts +42 -0
- package/dist/serena/code_editor.js +239 -0
- package/dist/serena/config/context_mode.d.ts +41 -0
- package/dist/serena/config/context_mode.js +239 -0
- package/dist/serena/config/serena_config.d.ts +134 -0
- package/dist/serena/config/serena_config.js +718 -0
- package/dist/serena/constants.d.ts +18 -0
- package/dist/serena/constants.js +27 -0
- package/dist/serena/dashboard.d.ts +55 -0
- package/dist/serena/dashboard.js +472 -0
- package/dist/serena/generated/generated_prompt_factory.d.ts +27 -0
- package/dist/serena/generated/generated_prompt_factory.js +42 -0
- package/dist/serena/gui_log_viewer.d.ts +41 -0
- package/dist/serena/gui_log_viewer.js +436 -0
- package/dist/serena/mcp.d.ts +118 -0
- package/dist/serena/mcp.js +904 -0
- package/dist/serena/project.d.ts +62 -0
- package/dist/serena/project.js +321 -0
- package/dist/serena/prompt_factory.d.ts +20 -0
- package/dist/serena/prompt_factory.js +42 -0
- package/dist/serena/resources/config/contexts/agent.yml +8 -0
- package/dist/serena/resources/config/contexts/chatgpt.yml +28 -0
- package/dist/serena/resources/config/contexts/codex.yml +27 -0
- package/dist/serena/resources/config/contexts/context.template.yml +11 -0
- package/dist/serena/resources/config/contexts/desktop-app.yml +17 -0
- package/dist/serena/resources/config/contexts/ide-assistant.yml +26 -0
- package/dist/serena/resources/config/contexts/oaicompat-agent.yml +8 -0
- package/dist/serena/resources/config/internal_modes/jetbrains.yml +15 -0
- package/dist/serena/resources/config/modes/editing.yml +112 -0
- package/dist/serena/resources/config/modes/interactive.yml +11 -0
- package/dist/serena/resources/config/modes/mode.template.yml +7 -0
- package/dist/serena/resources/config/modes/no-onboarding.yml +8 -0
- package/dist/serena/resources/config/modes/onboarding.yml +16 -0
- package/dist/serena/resources/config/modes/one-shot.yml +15 -0
- package/dist/serena/resources/config/modes/planning.yml +15 -0
- package/dist/serena/resources/config/prompt_templates/simple_tool_outputs.yml +75 -0
- package/dist/serena/resources/config/prompt_templates/system_prompt.yml +66 -0
- package/dist/serena/resources/dashboard/dashboard.js +815 -0
- package/dist/serena/resources/dashboard/index.html +314 -0
- package/dist/serena/resources/dashboard/jquery.min.js +3 -0
- package/dist/serena/resources/dashboard/serena-icon-16.png +0 -0
- package/dist/serena/resources/dashboard/serena-icon-32.png +0 -0
- package/dist/serena/resources/dashboard/serena-icon-48.png +0 -0
- package/dist/serena/resources/dashboard/serena-logs-dark-mode.png +0 -0
- package/dist/serena/resources/dashboard/serena-logs.png +0 -0
- package/dist/serena/resources/project.template.yml +67 -0
- package/dist/serena/resources/serena_config.template.yml +85 -0
- package/dist/serena/symbol.d.ts +199 -0
- package/dist/serena/symbol.js +616 -0
- package/dist/serena/text_utils.d.ts +51 -0
- package/dist/serena/text_utils.js +267 -0
- package/dist/serena/tools/cmd_tools.d.ts +31 -0
- package/dist/serena/tools/cmd_tools.js +48 -0
- package/dist/serena/tools/config_tools.d.ts +53 -0
- package/dist/serena/tools/config_tools.js +176 -0
- package/dist/serena/tools/file_tools.d.ts +231 -0
- package/dist/serena/tools/file_tools.js +511 -0
- package/dist/serena/tools/index.d.ts +7 -0
- package/dist/serena/tools/index.js +7 -0
- package/dist/serena/tools/memory_tools.d.ts +60 -0
- package/dist/serena/tools/memory_tools.js +135 -0
- package/dist/serena/tools/symbol_tools.d.ts +165 -0
- package/dist/serena/tools/symbol_tools.js +362 -0
- package/dist/serena/tools/tools_base.d.ts +162 -0
- package/dist/serena/tools/tools_base.js +378 -0
- package/dist/serena/tools/workflow_tools.d.ts +35 -0
- package/dist/serena/tools/workflow_tools.js +161 -0
- package/dist/serena/util/class_decorators.d.ts +7 -0
- package/dist/serena/util/class_decorators.js +37 -0
- package/dist/serena/util/exception.d.ts +8 -0
- package/dist/serena/util/exception.js +53 -0
- package/dist/serena/util/file_system.d.ts +30 -0
- package/dist/serena/util/file_system.js +352 -0
- package/dist/serena/util/general.d.ts +11 -0
- package/dist/serena/util/general.js +42 -0
- package/dist/serena/util/git.d.ts +11 -0
- package/dist/serena/util/git.js +37 -0
- package/dist/serena/util/inspection.d.ts +45 -0
- package/dist/serena/util/inspection.js +221 -0
- package/dist/serena/util/logging.d.ts +46 -0
- package/dist/serena/util/logging.js +205 -0
- package/dist/serena/util/shell.d.ts +21 -0
- package/dist/serena/util/shell.js +95 -0
- package/dist/serena/util/thread.d.ts +23 -0
- package/dist/serena/util/thread.js +88 -0
- package/dist/serena/version.d.ts +1 -0
- package/dist/serena/version.js +23 -0
- package/dist/solidlsp/language_servers/autoload.d.ts +23 -0
- package/dist/solidlsp/language_servers/autoload.js +25 -0
- package/dist/solidlsp/language_servers/bash_language_server.d.ts +10 -0
- package/dist/solidlsp/language_servers/bash_language_server.js +64 -0
- package/dist/solidlsp/language_servers/clangd_language_server.d.ts +13 -0
- package/dist/solidlsp/language_servers/clangd_language_server.js +110 -0
- package/dist/solidlsp/language_servers/clojure_lsp.d.ts +13 -0
- package/dist/solidlsp/language_servers/clojure_lsp.js +137 -0
- package/dist/solidlsp/language_servers/common.d.ts +41 -0
- package/dist/solidlsp/language_servers/common.js +365 -0
- package/dist/solidlsp/language_servers/csharp_language_server.d.ts +21 -0
- package/dist/solidlsp/language_servers/csharp_language_server.js +694 -0
- package/dist/solidlsp/language_servers/dart_language_server.d.ts +10 -0
- package/dist/solidlsp/language_servers/dart_language_server.js +122 -0
- package/dist/solidlsp/language_servers/eclipse_jdtls.d.ts +24 -0
- package/dist/solidlsp/language_servers/eclipse_jdtls.js +671 -0
- package/dist/solidlsp/language_servers/erlang_language_server.d.ts +22 -0
- package/dist/solidlsp/language_servers/erlang_language_server.js +327 -0
- package/dist/solidlsp/language_servers/gopls.d.ts +12 -0
- package/dist/solidlsp/language_servers/gopls.js +59 -0
- package/dist/solidlsp/language_servers/intelephense.d.ts +13 -0
- package/dist/solidlsp/language_servers/intelephense.js +121 -0
- package/dist/solidlsp/language_servers/jedi_server.d.ts +18 -0
- package/dist/solidlsp/language_servers/jedi_server.js +234 -0
- package/dist/solidlsp/language_servers/kotlin_language_server.d.ts +19 -0
- package/dist/solidlsp/language_servers/kotlin_language_server.js +474 -0
- package/dist/solidlsp/language_servers/lua_ls.d.ts +18 -0
- package/dist/solidlsp/language_servers/lua_ls.js +319 -0
- package/dist/solidlsp/language_servers/nixd_language_server.d.ts +17 -0
- package/dist/solidlsp/language_servers/nixd_language_server.js +341 -0
- package/dist/solidlsp/language_servers/pyright_server.d.ts +19 -0
- package/dist/solidlsp/language_servers/pyright_server.js +180 -0
- package/dist/solidlsp/language_servers/r_language_server.d.ts +19 -0
- package/dist/solidlsp/language_servers/r_language_server.js +184 -0
- package/dist/solidlsp/language_servers/ruby_common.d.ts +10 -0
- package/dist/solidlsp/language_servers/ruby_common.js +136 -0
- package/dist/solidlsp/language_servers/ruby_lsp.d.ts +18 -0
- package/dist/solidlsp/language_servers/ruby_lsp.js +230 -0
- package/dist/solidlsp/language_servers/rust_analyzer.d.ts +13 -0
- package/dist/solidlsp/language_servers/rust_analyzer.js +96 -0
- package/dist/solidlsp/language_servers/solargraph.d.ts +18 -0
- package/dist/solidlsp/language_servers/solargraph.js +208 -0
- package/dist/solidlsp/language_servers/sourcekit_lsp.d.ts +24 -0
- package/dist/solidlsp/language_servers/sourcekit_lsp.js +449 -0
- package/dist/solidlsp/language_servers/terraform_ls.d.ts +13 -0
- package/dist/solidlsp/language_servers/terraform_ls.js +139 -0
- package/dist/solidlsp/language_servers/typescript_language_server.d.ts +20 -0
- package/dist/solidlsp/language_servers/typescript_language_server.js +237 -0
- package/dist/solidlsp/language_servers/vts_language_server.d.ts +13 -0
- package/dist/solidlsp/language_servers/vts_language_server.js +121 -0
- package/dist/solidlsp/language_servers/zls.d.ts +20 -0
- package/dist/solidlsp/language_servers/zls.js +254 -0
- package/dist/solidlsp/ls.d.ts +197 -0
- package/dist/solidlsp/ls.js +507 -0
- package/dist/solidlsp/ls_config.d.ts +43 -0
- package/dist/solidlsp/ls_config.js +157 -0
- package/dist/solidlsp/ls_exceptions.d.ts +5 -0
- package/dist/solidlsp/ls_exceptions.js +14 -0
- package/dist/solidlsp/ls_handler.d.ts +54 -0
- package/dist/solidlsp/ls_handler.js +406 -0
- package/dist/solidlsp/ls_request.d.ts +31 -0
- package/dist/solidlsp/ls_request.js +42 -0
- package/dist/solidlsp/ls_types.d.ts +7 -0
- package/dist/solidlsp/ls_types.js +8 -0
- package/dist/solidlsp/lsp_protocol_handler/server.d.ts +61 -0
- package/dist/solidlsp/lsp_protocol_handler/server.js +68 -0
- package/dist/solidlsp/util/subprocess_util.d.ts +6 -0
- package/dist/solidlsp/util/subprocess_util.js +11 -0
- package/dist/solidlsp/util/zip.d.ts +25 -0
- package/dist/solidlsp/util/zip.js +188 -0
- package/package.json +65 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { ErrorCode, McpError, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { createServer } from 'node:http';
|
|
7
|
+
import { URL as NodeURL } from 'node:url';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
10
|
+
import { DEFAULT_CONTEXT, DEFAULT_MODES } from './constants.js';
|
|
11
|
+
import { SerenaAgentContext, SerenaAgentMode } from './config/context_mode.js';
|
|
12
|
+
import { SerenaConfig } from './config/serena_config.js';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { showFatalExceptionSafe } from './util/exception.js';
|
|
15
|
+
import { createSerenaLogger } from './util/logging.js';
|
|
16
|
+
import { SerenaAgent } from './agent.js';
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const packageJson = require('../../package.json');
|
|
19
|
+
const { logger: log } = createSerenaLogger({ name: 'serena.mcp' });
|
|
20
|
+
function unwrapZodObject(schema) {
|
|
21
|
+
if (!schema) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (schema instanceof z.ZodObject) {
|
|
25
|
+
return schema;
|
|
26
|
+
}
|
|
27
|
+
if (schema instanceof z.ZodEffects) {
|
|
28
|
+
const inner = schema._def.schema;
|
|
29
|
+
return unwrapZodObject(inner);
|
|
30
|
+
}
|
|
31
|
+
if (schema instanceof z.ZodDefault) {
|
|
32
|
+
const inner = schema._def.innerType;
|
|
33
|
+
return unwrapZodObject(inner);
|
|
34
|
+
}
|
|
35
|
+
if (schema instanceof z.ZodNullable || schema instanceof z.ZodOptional) {
|
|
36
|
+
const base = schema.unwrap();
|
|
37
|
+
return unwrapZodObject(base);
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function toZodRawShape(schema) {
|
|
42
|
+
const objectSchema = unwrapZodObject(schema);
|
|
43
|
+
if (!objectSchema) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return objectSchema.shape;
|
|
47
|
+
}
|
|
48
|
+
function isJsonObject(value) {
|
|
49
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
function deepCloneJsonValue(value) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.map((item) => deepCloneJsonValue(item));
|
|
54
|
+
}
|
|
55
|
+
if (isJsonObject(value)) {
|
|
56
|
+
const clone = {};
|
|
57
|
+
for (const [key, child] of Object.entries(value)) {
|
|
58
|
+
clone[key] = child === undefined ? undefined : deepCloneJsonValue(child);
|
|
59
|
+
}
|
|
60
|
+
return clone;
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function cloneJsonSchema(schema) {
|
|
65
|
+
const structured = globalThis.structuredClone;
|
|
66
|
+
if (typeof structured === 'function') {
|
|
67
|
+
return structured(schema);
|
|
68
|
+
}
|
|
69
|
+
return deepCloneJsonValue(schema);
|
|
70
|
+
}
|
|
71
|
+
function isRecord(value) {
|
|
72
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
function sanitizeJsonSchemaForOpenAi(schema) {
|
|
75
|
+
const cloned = cloneJsonSchema(schema);
|
|
76
|
+
const walk = (node) => {
|
|
77
|
+
if (Array.isArray(node)) {
|
|
78
|
+
return node.map((item) => walk(item));
|
|
79
|
+
}
|
|
80
|
+
if (isJsonObject(node)) {
|
|
81
|
+
const record = node;
|
|
82
|
+
const typeValue = record['type'];
|
|
83
|
+
if (typeof typeValue === 'string') {
|
|
84
|
+
if (typeValue === 'integer') {
|
|
85
|
+
record['type'] = 'number';
|
|
86
|
+
if (record['multipleOf'] === undefined) {
|
|
87
|
+
record['multipleOf'] = 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (Array.isArray(typeValue)) {
|
|
92
|
+
const stringEntries = typeValue.filter((entry) => typeof entry === 'string');
|
|
93
|
+
const filteredStrings = stringEntries
|
|
94
|
+
.map((entry) => (entry === 'integer' ? 'number' : entry))
|
|
95
|
+
.filter((entry) => entry !== 'null');
|
|
96
|
+
if (filteredStrings.length === 0) {
|
|
97
|
+
record['type'] = 'object';
|
|
98
|
+
}
|
|
99
|
+
else if (filteredStrings.length === 1) {
|
|
100
|
+
record['type'] = filteredStrings[0];
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
record['type'] = filteredStrings;
|
|
104
|
+
}
|
|
105
|
+
if (stringEntries.includes('integer') || filteredStrings.includes('number')) {
|
|
106
|
+
record['multipleOf'] = record['multipleOf'] ?? 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const enumValue = record['enum'];
|
|
110
|
+
if (Array.isArray(enumValue)) {
|
|
111
|
+
const values = enumValue;
|
|
112
|
+
if (values.length > 0 && values.every((value) => typeof value === 'number' && Number.isInteger(value))) {
|
|
113
|
+
record['type'] = record['type'] ?? 'number';
|
|
114
|
+
record['multipleOf'] = record['multipleOf'] ?? 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const key of ['oneOf', 'anyOf']) {
|
|
118
|
+
const value = record[key];
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
const sanitized = value.map((entry) => walk(entry));
|
|
121
|
+
if (sanitized.length === 2) {
|
|
122
|
+
const types = sanitized.map((entry) => (isJsonObject(entry) ? entry['type'] : undefined));
|
|
123
|
+
if (types.includes('null')) {
|
|
124
|
+
const nonNullIndex = types.findIndex((type) => type !== 'null');
|
|
125
|
+
const nonNull = nonNullIndex >= 0 ? sanitized[nonNullIndex] : undefined;
|
|
126
|
+
if (isJsonObject(nonNull)) {
|
|
127
|
+
Object.assign(record, nonNull);
|
|
128
|
+
delete record[key];
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const serialized = sanitized.map((entry) => JSON.stringify(entry));
|
|
135
|
+
const unique = new Set(serialized);
|
|
136
|
+
if (unique.size === 1) {
|
|
137
|
+
const only = sanitized[0];
|
|
138
|
+
if (isJsonObject(only)) {
|
|
139
|
+
Object.assign(record, only);
|
|
140
|
+
delete record[key];
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Keep original structure if serialization fails.
|
|
147
|
+
}
|
|
148
|
+
record[key] = sanitized;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const [key, value] of Object.entries(record)) {
|
|
152
|
+
if (value === undefined) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(value) || isJsonObject(value)) {
|
|
156
|
+
record[key] = walk(value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return record;
|
|
160
|
+
}
|
|
161
|
+
return node;
|
|
162
|
+
};
|
|
163
|
+
return walk(cloned);
|
|
164
|
+
}
|
|
165
|
+
function maybeCreateJsonSchema(schema, options) {
|
|
166
|
+
const objectSchema = unwrapZodObject(schema);
|
|
167
|
+
if (!objectSchema) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const jsonSchema = zodToJsonSchema(objectSchema, {
|
|
171
|
+
name: options.name,
|
|
172
|
+
target: 'jsonSchema7',
|
|
173
|
+
$refStrategy: 'none'
|
|
174
|
+
});
|
|
175
|
+
if (!jsonSchema || typeof jsonSchema !== 'object') {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (options.sanitizeForOpenAiTools) {
|
|
179
|
+
return sanitizeJsonSchemaForOpenAi(jsonSchema);
|
|
180
|
+
}
|
|
181
|
+
return jsonSchema;
|
|
182
|
+
}
|
|
183
|
+
function createDefaultServerInfo() {
|
|
184
|
+
const version = typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
|
|
185
|
+
return {
|
|
186
|
+
name: 'FastMCP',
|
|
187
|
+
version
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/*
|
|
191
|
+
* NOTE: The MCP SDK currently ships without fully typed ESM re-exports, which
|
|
192
|
+
* causes TypeScript to treat several helper methods as `any`. We perform
|
|
193
|
+
* runtime guards before calling into the SDK and suppress the lint warnings
|
|
194
|
+
* locally to avoid scattering disable directives at call sites.
|
|
195
|
+
*/
|
|
196
|
+
function registerToolWithServer(server, options) {
|
|
197
|
+
const toolMethod = server.tool.bind(server);
|
|
198
|
+
const registered = options.annotations && Object.keys(options.annotations).length > 0
|
|
199
|
+
? toolMethod(options.name, options.description, options.inputSchema, options.annotations, options.callback)
|
|
200
|
+
: toolMethod(options.name, options.description, options.inputSchema, options.callback);
|
|
201
|
+
if (options.outputSchema) {
|
|
202
|
+
registered.update({ outputSchema: options.outputSchema });
|
|
203
|
+
}
|
|
204
|
+
const serverWithTracking = server;
|
|
205
|
+
serverWithTracking.__serenaRegisteredTools ??= new Map();
|
|
206
|
+
const annotations = options.annotations && Object.keys(options.annotations).length > 0 ? options.annotations : undefined;
|
|
207
|
+
serverWithTracking.__serenaRegisteredTools.set(options.name, {
|
|
208
|
+
description: options.description,
|
|
209
|
+
annotations,
|
|
210
|
+
_meta: undefined,
|
|
211
|
+
callback: options.callback
|
|
212
|
+
});
|
|
213
|
+
return registered;
|
|
214
|
+
}
|
|
215
|
+
export class SerenaMCPFactory {
|
|
216
|
+
context;
|
|
217
|
+
project;
|
|
218
|
+
agent = null;
|
|
219
|
+
requestContext = null;
|
|
220
|
+
constructor(context = DEFAULT_CONTEXT, project = null) {
|
|
221
|
+
this.context = SerenaAgentContext.load(context);
|
|
222
|
+
this.project = project ?? null;
|
|
223
|
+
}
|
|
224
|
+
getAgent() {
|
|
225
|
+
if (!this.agent) {
|
|
226
|
+
throw new Error('Serena agent has not been instantiated yet.');
|
|
227
|
+
}
|
|
228
|
+
return this.agent;
|
|
229
|
+
}
|
|
230
|
+
setAgent(agent) {
|
|
231
|
+
this.agent = agent;
|
|
232
|
+
this.requestContext = { agent };
|
|
233
|
+
}
|
|
234
|
+
getRequestContext() {
|
|
235
|
+
if (!this.requestContext) {
|
|
236
|
+
throw new Error('Request context not initialized.');
|
|
237
|
+
}
|
|
238
|
+
return this.requestContext;
|
|
239
|
+
}
|
|
240
|
+
isOpenAiCompatibleContext() {
|
|
241
|
+
return ['chatgpt', 'codex', 'oaicompat-agent'].includes(this.context.name);
|
|
242
|
+
}
|
|
243
|
+
registerDefaultCapabilities(mcpServer) {
|
|
244
|
+
const server = mcpServer.server;
|
|
245
|
+
server.registerCapabilities({
|
|
246
|
+
experimental: {},
|
|
247
|
+
prompts: {
|
|
248
|
+
listChanged: false
|
|
249
|
+
},
|
|
250
|
+
resources: {
|
|
251
|
+
subscribe: false,
|
|
252
|
+
listChanged: false
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
const agent = this.getAgent();
|
|
256
|
+
const promptFactory = agent.promptFactory;
|
|
257
|
+
const memoriesManager = agent.memoriesManager;
|
|
258
|
+
server.setRequestHandler(ListPromptsRequestSchema, () => ({
|
|
259
|
+
prompts: this.buildPromptMetadata(promptFactory)
|
|
260
|
+
}));
|
|
261
|
+
server.setRequestHandler(GetPromptRequestSchema, (request) => this.buildPromptResult(promptFactory, request.params.name, request.params.arguments ?? {}));
|
|
262
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
263
|
+
resources: await this.buildResourceList(memoriesManager)
|
|
264
|
+
}));
|
|
265
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({
|
|
266
|
+
resourceTemplates: this.buildResourceTemplates(memoriesManager)
|
|
267
|
+
}));
|
|
268
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => this.readResource(memoriesManager, request.params.uri));
|
|
269
|
+
}
|
|
270
|
+
buildPromptMetadata(promptFactory) {
|
|
271
|
+
const names = promptFactory.listPromptTemplateNames();
|
|
272
|
+
return names.map((name) => {
|
|
273
|
+
const params = promptFactory.getPromptTemplateParameters(name);
|
|
274
|
+
return {
|
|
275
|
+
name,
|
|
276
|
+
description: this.describePromptTemplate(name),
|
|
277
|
+
arguments: params.length === 0
|
|
278
|
+
? undefined
|
|
279
|
+
: params.map((param) => ({
|
|
280
|
+
name: param,
|
|
281
|
+
required: true
|
|
282
|
+
}))
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
buildPromptResult(promptFactory, promptName, rawArgs) {
|
|
287
|
+
if (!promptFactory.hasPromptTemplate(promptName)) {
|
|
288
|
+
throw new McpError(ErrorCode.InvalidParams, `Prompt ${promptName} not found`);
|
|
289
|
+
}
|
|
290
|
+
const requiredParams = promptFactory.getPromptTemplateParameters(promptName);
|
|
291
|
+
const missingParams = requiredParams.filter((param) => !(param in rawArgs));
|
|
292
|
+
if (missingParams.length > 0) {
|
|
293
|
+
throw new McpError(ErrorCode.InvalidParams, `Missing required arguments for prompt ${promptName}: ${missingParams.join(', ')}`);
|
|
294
|
+
}
|
|
295
|
+
const rendered = promptFactory.renderPrompt(promptName, rawArgs);
|
|
296
|
+
return {
|
|
297
|
+
description: this.describePromptTemplate(promptName),
|
|
298
|
+
messages: [
|
|
299
|
+
{
|
|
300
|
+
role: 'assistant',
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: 'text',
|
|
304
|
+
text: rendered
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
describePromptTemplate(name) {
|
|
312
|
+
const readable = name.replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
313
|
+
if (!readable) {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
return readable.charAt(0).toUpperCase() + readable.slice(1);
|
|
317
|
+
}
|
|
318
|
+
async buildResourceList(manager) {
|
|
319
|
+
if (!manager) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const names = await this.listMemories(manager);
|
|
323
|
+
return names.map((name) => ({
|
|
324
|
+
uri: this.formatMemoryUri(name),
|
|
325
|
+
name,
|
|
326
|
+
description: 'Serena project memory entry',
|
|
327
|
+
mimeType: 'text/markdown'
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
buildResourceTemplates(manager) {
|
|
331
|
+
if (!manager) {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
return [
|
|
335
|
+
{
|
|
336
|
+
name: 'serena-memory',
|
|
337
|
+
uriTemplate: 'serena://memory/{name}',
|
|
338
|
+
description: 'Template for accessing Serena project memory entries',
|
|
339
|
+
mimeType: 'text/markdown'
|
|
340
|
+
}
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
async readResource(manager, uri) {
|
|
344
|
+
const memoryName = this.parseMemoryUri(uri);
|
|
345
|
+
if (!memoryName) {
|
|
346
|
+
throw new McpError(ErrorCode.InvalidParams, `Unsupported resource URI: ${uri}`);
|
|
347
|
+
}
|
|
348
|
+
if (!manager) {
|
|
349
|
+
throw new McpError(ErrorCode.InvalidParams, 'Memory store is unavailable');
|
|
350
|
+
}
|
|
351
|
+
const content = await this.loadMemory(manager, memoryName);
|
|
352
|
+
return {
|
|
353
|
+
contents: [
|
|
354
|
+
{
|
|
355
|
+
type: 'text',
|
|
356
|
+
text: content,
|
|
357
|
+
mimeType: 'text/markdown'
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
parseMemoryUri(uri) {
|
|
363
|
+
if (!uri.toLowerCase().startsWith('serena://memory/')) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const suffix = uri.slice('serena://memory/'.length);
|
|
367
|
+
if (!suffix) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
return decodeURIComponent(suffix);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return suffix;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
formatMemoryUri(name) {
|
|
378
|
+
return `serena://memory/${encodeURIComponent(name)}`;
|
|
379
|
+
}
|
|
380
|
+
async listMemories(manager) {
|
|
381
|
+
const listFn = this.bindManagerFunction(manager, ['listMemories', 'list_memories']);
|
|
382
|
+
const raw = await Promise.resolve(listFn());
|
|
383
|
+
if (!raw) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
if (Array.isArray(raw)) {
|
|
387
|
+
return raw.map((entry) => this.stringifyUnknown(entry));
|
|
388
|
+
}
|
|
389
|
+
if (this.isIterable(raw)) {
|
|
390
|
+
const result = [];
|
|
391
|
+
for (const entry of raw) {
|
|
392
|
+
result.push(this.stringifyUnknown(entry));
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
return [this.stringifyUnknown(raw)];
|
|
397
|
+
}
|
|
398
|
+
async loadMemory(manager, name) {
|
|
399
|
+
const loadFn = this.bindManagerFunction(manager, ['loadMemory', 'load_memory']);
|
|
400
|
+
const raw = await Promise.resolve(loadFn(name));
|
|
401
|
+
const text = this.stringifyUnknown(raw);
|
|
402
|
+
return text;
|
|
403
|
+
}
|
|
404
|
+
bindManagerFunction(manager, candidateNames) {
|
|
405
|
+
for (const candidate of candidateNames) {
|
|
406
|
+
const fn = Reflect.get(manager, candidate);
|
|
407
|
+
if (typeof fn === 'function') {
|
|
408
|
+
return (...fnArgs) => fn.apply(manager, fnArgs);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
throw new Error(`Memories manager does not implement any of: ${candidateNames.join(', ')}`);
|
|
412
|
+
}
|
|
413
|
+
stringifyUnknown(value) {
|
|
414
|
+
if (value === undefined || value === null) {
|
|
415
|
+
return '';
|
|
416
|
+
}
|
|
417
|
+
if (typeof value === 'string') {
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
421
|
+
return value.toString();
|
|
422
|
+
}
|
|
423
|
+
if (typeof value === 'symbol') {
|
|
424
|
+
return value.toString();
|
|
425
|
+
}
|
|
426
|
+
if (typeof value === 'function') {
|
|
427
|
+
return '[function]';
|
|
428
|
+
}
|
|
429
|
+
if (typeof value === 'object') {
|
|
430
|
+
try {
|
|
431
|
+
return JSON.stringify(value);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return Object.prototype.toString.call(value);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return '[unrecognized]';
|
|
438
|
+
}
|
|
439
|
+
isIterable(value) {
|
|
440
|
+
return typeof value === 'object' && value !== null && Symbol.iterator in value;
|
|
441
|
+
}
|
|
442
|
+
registerTools(server, openAiToolCompatible) {
|
|
443
|
+
const tools = Array.from(this.iterTools());
|
|
444
|
+
for (const tool of tools) {
|
|
445
|
+
this.registerTool(server, tool, openAiToolCompatible);
|
|
446
|
+
}
|
|
447
|
+
log.info(`Registered ${tools.length} MCP tools: ${tools.map((tool) => tool.getName()).join(', ')}`);
|
|
448
|
+
}
|
|
449
|
+
registerTool(server, tool, openAiToolCompatible) {
|
|
450
|
+
const toolName = tool.getName();
|
|
451
|
+
const metadata = tool.getApplyFnMetadata();
|
|
452
|
+
const descriptionOverride = this.context.toolDescriptionOverrides[toolName];
|
|
453
|
+
let description = descriptionOverride ?? metadata.description ?? '';
|
|
454
|
+
description = description.trim();
|
|
455
|
+
if (description.length > 0 && !description.endsWith('.')) {
|
|
456
|
+
description += '.';
|
|
457
|
+
}
|
|
458
|
+
const inputSchemaShape = toZodRawShape(metadata.inputSchema);
|
|
459
|
+
if (!inputSchemaShape) {
|
|
460
|
+
throw new Error(`Tool ${toolName} must expose a Zod object input schema.`);
|
|
461
|
+
}
|
|
462
|
+
const outputSchemaShape = toZodRawShape(metadata.outputSchema);
|
|
463
|
+
const inputJsonSchema = openAiToolCompatible
|
|
464
|
+
? maybeCreateJsonSchema(metadata.inputSchema, {
|
|
465
|
+
name: `${toolName}Input`,
|
|
466
|
+
sanitizeForOpenAiTools: true
|
|
467
|
+
})
|
|
468
|
+
: undefined;
|
|
469
|
+
const outputJsonSchema = metadata.outputSchema
|
|
470
|
+
? maybeCreateJsonSchema(metadata.outputSchema, {
|
|
471
|
+
name: `${toolName}Output`,
|
|
472
|
+
sanitizeForOpenAiTools: openAiToolCompatible
|
|
473
|
+
})
|
|
474
|
+
: undefined;
|
|
475
|
+
const annotations = openAiToolCompatible
|
|
476
|
+
? {
|
|
477
|
+
'serena/openaiToolCompatible': true,
|
|
478
|
+
...(inputJsonSchema ? { 'serena/openaiToolInputSchema': inputJsonSchema } : {}),
|
|
479
|
+
...(outputJsonSchema ? { 'serena/openaiToolOutputSchema': outputJsonSchema } : {})
|
|
480
|
+
}
|
|
481
|
+
: undefined;
|
|
482
|
+
const callback = async (args, extra) => {
|
|
483
|
+
try {
|
|
484
|
+
const normalizedArgs = args;
|
|
485
|
+
log.debug('MCP invoking tool', {
|
|
486
|
+
tool: toolName,
|
|
487
|
+
normalizedArgs,
|
|
488
|
+
hasAgent: this.agent !== null
|
|
489
|
+
});
|
|
490
|
+
const result = await tool.applyEx(normalizedArgs, { logCall: true, catchExceptions: true });
|
|
491
|
+
log.debug('Tool result before structured output handling', {
|
|
492
|
+
tool: toolName,
|
|
493
|
+
mode: metadata.structuredOutput ? 'structured' : 'text',
|
|
494
|
+
result
|
|
495
|
+
});
|
|
496
|
+
if (metadata.structuredOutput && outputJsonSchema && metadata.outputSchema) {
|
|
497
|
+
let parsed;
|
|
498
|
+
try {
|
|
499
|
+
parsed = JSON.parse(result);
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: 'text',
|
|
507
|
+
text: `Failed to parse structured output from tool ${toolName}: ${message}`
|
|
508
|
+
}
|
|
509
|
+
],
|
|
510
|
+
isError: true
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (!isRecord(parsed)) {
|
|
514
|
+
return {
|
|
515
|
+
content: [
|
|
516
|
+
{
|
|
517
|
+
type: 'text',
|
|
518
|
+
text: `Tool ${toolName} must return a JSON object when structuredOutput is enabled.`
|
|
519
|
+
}
|
|
520
|
+
],
|
|
521
|
+
isError: true
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
content: [],
|
|
526
|
+
structuredContent: parsed
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
content: [
|
|
531
|
+
{
|
|
532
|
+
type: 'text',
|
|
533
|
+
text: result
|
|
534
|
+
}
|
|
535
|
+
]
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
log.error('MCP tool execution threw an unexpected error', { err: error, tool: toolName, extra });
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const registered = registerToolWithServer(server, {
|
|
544
|
+
name: toolName,
|
|
545
|
+
description,
|
|
546
|
+
inputSchema: inputSchemaShape,
|
|
547
|
+
annotations,
|
|
548
|
+
callback,
|
|
549
|
+
outputSchema: outputSchemaShape
|
|
550
|
+
});
|
|
551
|
+
return registered;
|
|
552
|
+
}
|
|
553
|
+
createMcpServer(options = {}) {
|
|
554
|
+
try {
|
|
555
|
+
const config = SerenaConfig.fromConfigFile();
|
|
556
|
+
if (options.enableWebDashboard !== undefined && options.enableWebDashboard !== null) {
|
|
557
|
+
config.webDashboard = options.enableWebDashboard;
|
|
558
|
+
}
|
|
559
|
+
if (options.enableGuiLogWindow !== undefined && options.enableGuiLogWindow !== null) {
|
|
560
|
+
config.guiLogWindowEnabled = options.enableGuiLogWindow;
|
|
561
|
+
}
|
|
562
|
+
if (options.logLevel) {
|
|
563
|
+
const normalized = options.logLevel.toUpperCase();
|
|
564
|
+
const level = LOG_LEVEL_MAP[normalized] ?? LOG_LEVEL_MAP.INFO;
|
|
565
|
+
config.logLevel = level;
|
|
566
|
+
}
|
|
567
|
+
if (options.traceLspCommunication !== undefined && options.traceLspCommunication !== null) {
|
|
568
|
+
config.traceLspCommunication = options.traceLspCommunication;
|
|
569
|
+
}
|
|
570
|
+
if (options.toolTimeout !== undefined && options.toolTimeout !== null) {
|
|
571
|
+
config.toolTimeout = options.toolTimeout;
|
|
572
|
+
}
|
|
573
|
+
const modesInput = options.modes ? Array.from(options.modes) : Array.from(DEFAULT_MODES);
|
|
574
|
+
const modes = modesInput.map((mode) => SerenaAgentMode.load(mode));
|
|
575
|
+
const agent = this.instantiateAgent({
|
|
576
|
+
serenaConfig: config,
|
|
577
|
+
modes
|
|
578
|
+
});
|
|
579
|
+
this.setAgent(agent);
|
|
580
|
+
const instructions = options.instructionsOverride !== undefined
|
|
581
|
+
? options.instructionsOverride ?? ''
|
|
582
|
+
: this.getInitialInstructions();
|
|
583
|
+
const serverInfo = options.serverInfo ?? createDefaultServerInfo();
|
|
584
|
+
const serverOptions = {
|
|
585
|
+
...(options.serverOptions ?? {}),
|
|
586
|
+
instructions
|
|
587
|
+
};
|
|
588
|
+
const mcpServer = new McpServer(serverInfo, serverOptions);
|
|
589
|
+
this.registerTools(mcpServer, this.isOpenAiCompatibleContext());
|
|
590
|
+
this.registerDefaultCapabilities(mcpServer);
|
|
591
|
+
return mcpServer;
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
void showFatalExceptionSafe(error);
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
getLogger() {
|
|
599
|
+
return log;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const LOG_LEVEL_MAP = {
|
|
603
|
+
DEBUG: 10,
|
|
604
|
+
INFO: 20,
|
|
605
|
+
WARNING: 30,
|
|
606
|
+
ERROR: 40,
|
|
607
|
+
CRITICAL: 50
|
|
608
|
+
};
|
|
609
|
+
export class SerenaMCPFactorySingleProcess extends SerenaMCPFactory {
|
|
610
|
+
memoryLogHandler;
|
|
611
|
+
agentFactory;
|
|
612
|
+
cachedTools = null;
|
|
613
|
+
constructor(options = {}) {
|
|
614
|
+
super(options.context, options.project ?? null);
|
|
615
|
+
this.memoryLogHandler = options.memoryLogHandler ?? null;
|
|
616
|
+
this.agentFactory =
|
|
617
|
+
options.agentFactory ??
|
|
618
|
+
((factoryOptions) => new SerenaAgent({
|
|
619
|
+
project: factoryOptions.project,
|
|
620
|
+
serenaConfig: factoryOptions.serenaConfig,
|
|
621
|
+
context: factoryOptions.context,
|
|
622
|
+
modes: factoryOptions.modes,
|
|
623
|
+
memoryLogHandler: factoryOptions.memoryLogHandler ?? undefined
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
instantiateAgent({ serenaConfig, modes }) {
|
|
627
|
+
if (!this.agentFactory) {
|
|
628
|
+
throw new Error('SerenaAgent factory not provided. Inject a factory via SerenaMCPFactorySingleProcessOptions.agentFactory once the agent implementation is available.');
|
|
629
|
+
}
|
|
630
|
+
const agent = this.agentFactory({
|
|
631
|
+
project: this.project,
|
|
632
|
+
serenaConfig,
|
|
633
|
+
context: this.context,
|
|
634
|
+
modes,
|
|
635
|
+
memoryLogHandler: this.memoryLogHandler
|
|
636
|
+
});
|
|
637
|
+
const toolProvider = agent;
|
|
638
|
+
if (typeof toolProvider.getExposedToolInstances === 'function') {
|
|
639
|
+
this.cachedTools = Array.from(toolProvider.getExposedToolInstances());
|
|
640
|
+
}
|
|
641
|
+
else if (typeof toolProvider.get_exposed_tool_instances === 'function') {
|
|
642
|
+
this.cachedTools = Array.from(toolProvider.get_exposed_tool_instances());
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
this.cachedTools = null;
|
|
646
|
+
}
|
|
647
|
+
return agent;
|
|
648
|
+
}
|
|
649
|
+
iterTools() {
|
|
650
|
+
if (this.cachedTools !== null) {
|
|
651
|
+
return this.cachedTools;
|
|
652
|
+
}
|
|
653
|
+
const agent = this.getAgent();
|
|
654
|
+
if (typeof agent.getExposedToolInstances === 'function') {
|
|
655
|
+
const tools = Array.from(agent.getExposedToolInstances());
|
|
656
|
+
this.cachedTools = tools;
|
|
657
|
+
return tools;
|
|
658
|
+
}
|
|
659
|
+
if (typeof agent.get_exposed_tool_instances === 'function') {
|
|
660
|
+
const tools = Array.from(agent.get_exposed_tool_instances());
|
|
661
|
+
this.cachedTools = tools;
|
|
662
|
+
return tools;
|
|
663
|
+
}
|
|
664
|
+
throw new Error('Agent does not expose tool discovery methods.');
|
|
665
|
+
}
|
|
666
|
+
getInitialInstructions() {
|
|
667
|
+
const agent = this.getAgent();
|
|
668
|
+
if (typeof agent.createSystemPrompt === 'function') {
|
|
669
|
+
const prompt = agent.createSystemPrompt();
|
|
670
|
+
if (typeof prompt === 'string' && prompt.length > 0) {
|
|
671
|
+
return prompt;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (typeof agent.create_system_prompt === 'function') {
|
|
675
|
+
const prompt = agent.create_system_prompt();
|
|
676
|
+
if (typeof prompt === 'string' && prompt.length > 0) {
|
|
677
|
+
return prompt;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
throw new Error('Agent did not provide a system prompt.');
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const DEFAULT_HTTP_PATH = '/mcp';
|
|
684
|
+
function normalizeHttpPath(path) {
|
|
685
|
+
if (path === undefined || path === null || path === '') {
|
|
686
|
+
return DEFAULT_HTTP_PATH;
|
|
687
|
+
}
|
|
688
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
689
|
+
}
|
|
690
|
+
function resolveRequestPath(req, defaultHost) {
|
|
691
|
+
if (!req.url) {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const hostHeader = req.headers.host ?? defaultHost;
|
|
696
|
+
const parsed = new NodeURL(req.url, `http://${hostHeader}`);
|
|
697
|
+
return parsed.pathname;
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function respondWithJson(res, statusCode, body) {
|
|
704
|
+
if (res.headersSent) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
res.writeHead(statusCode, {
|
|
708
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
709
|
+
'Cache-Control': 'no-store'
|
|
710
|
+
});
|
|
711
|
+
res.end(JSON.stringify(body));
|
|
712
|
+
}
|
|
713
|
+
export function createSerenaHttpRequestHandler({ transport, path, defaultHost }) {
|
|
714
|
+
const normalizedPath = normalizeHttpPath(path);
|
|
715
|
+
const fallbackHost = defaultHost ?? '127.0.0.1';
|
|
716
|
+
return async (req, res) => {
|
|
717
|
+
const requestPath = resolveRequestPath(req, fallbackHost);
|
|
718
|
+
if (requestPath !== normalizedPath) {
|
|
719
|
+
respondWithJson(res, 404, {
|
|
720
|
+
jsonrpc: '2.0',
|
|
721
|
+
error: {
|
|
722
|
+
code: -32601,
|
|
723
|
+
message: 'Not Found'
|
|
724
|
+
},
|
|
725
|
+
id: null
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
await transport.handleRequest(req, res);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
log.error('Unhandled error while processing HTTP MCP request', { err: error });
|
|
734
|
+
if (!res.headersSent) {
|
|
735
|
+
respondWithJson(res, 500, {
|
|
736
|
+
jsonrpc: '2.0',
|
|
737
|
+
error: {
|
|
738
|
+
code: -32000,
|
|
739
|
+
message: 'Internal Server Error',
|
|
740
|
+
data: error instanceof Error ? error.message : String(error)
|
|
741
|
+
},
|
|
742
|
+
id: null
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
try {
|
|
747
|
+
res.end();
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
// ignore secondary failures when the connection is already broken
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
async function listenHttpServer(server, port, host) {
|
|
757
|
+
await new Promise((resolve, reject) => {
|
|
758
|
+
const onError = (error) => {
|
|
759
|
+
server.off('listening', onListening);
|
|
760
|
+
reject(error);
|
|
761
|
+
};
|
|
762
|
+
const onListening = () => {
|
|
763
|
+
server.off('error', onError);
|
|
764
|
+
resolve();
|
|
765
|
+
};
|
|
766
|
+
server.once('error', onError);
|
|
767
|
+
server.listen(port, host, onListening);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
async function closeHttpServer(server) {
|
|
771
|
+
await new Promise((resolve, reject) => {
|
|
772
|
+
server.close((error) => {
|
|
773
|
+
if (error) {
|
|
774
|
+
reject(error);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
resolve();
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
function deriveServerUrl(address, path, fallbackHost) {
|
|
782
|
+
if (!address) {
|
|
783
|
+
throw new Error('Failed to determine HTTP server address.');
|
|
784
|
+
}
|
|
785
|
+
if (typeof address === 'string') {
|
|
786
|
+
const normalized = address.endsWith('/') ? address.slice(0, -1) : address;
|
|
787
|
+
if (normalized.startsWith('http://') || normalized.startsWith('https://')) {
|
|
788
|
+
return new NodeURL(`${normalized}${path}`);
|
|
789
|
+
}
|
|
790
|
+
return new NodeURL(`http://${fallbackHost}${path}`);
|
|
791
|
+
}
|
|
792
|
+
const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;
|
|
793
|
+
const resolvedHost = host === '::' || host === '0.0.0.0' ? fallbackHost : host;
|
|
794
|
+
return new NodeURL(`http://${resolvedHost}:${address.port}${path}`);
|
|
795
|
+
}
|
|
796
|
+
export async function createSerenaHttpServer(factory, options = {}) {
|
|
797
|
+
const host = options.host ?? '127.0.0.1';
|
|
798
|
+
const port = options.port ?? 0;
|
|
799
|
+
const path = normalizeHttpPath(options.path);
|
|
800
|
+
const transportOptions = options.transportOptions ?? {};
|
|
801
|
+
const sessionIdGenerator = options.sessionIdGenerator === null
|
|
802
|
+
? undefined
|
|
803
|
+
: options.sessionIdGenerator ?? transportOptions.sessionIdGenerator ?? (() => randomUUID());
|
|
804
|
+
const httpTransport = new StreamableHTTPServerTransport({
|
|
805
|
+
...transportOptions,
|
|
806
|
+
sessionIdGenerator
|
|
807
|
+
});
|
|
808
|
+
const mcpServer = factory.createMcpServer(options);
|
|
809
|
+
await mcpServer.connect(httpTransport);
|
|
810
|
+
const handler = createSerenaHttpRequestHandler({
|
|
811
|
+
transport: httpTransport,
|
|
812
|
+
path,
|
|
813
|
+
defaultHost: host
|
|
814
|
+
});
|
|
815
|
+
const httpServer = createServer((req, res) => {
|
|
816
|
+
void handler(req, res);
|
|
817
|
+
});
|
|
818
|
+
await listenHttpServer(httpServer, port, host);
|
|
819
|
+
const addressInfo = httpServer.address();
|
|
820
|
+
const url = deriveServerUrl(addressInfo, path, host);
|
|
821
|
+
return {
|
|
822
|
+
httpServer,
|
|
823
|
+
transport: httpTransport,
|
|
824
|
+
mcpServer,
|
|
825
|
+
url,
|
|
826
|
+
async close() {
|
|
827
|
+
await Promise.allSettled([httpTransport.close(), mcpServer.close()]);
|
|
828
|
+
await closeHttpServer(httpServer);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
class SerenaStdioServerTransport extends StdioServerTransport {
|
|
833
|
+
stdin;
|
|
834
|
+
handleStreamEnd;
|
|
835
|
+
handleStreamClose;
|
|
836
|
+
closePromise = null;
|
|
837
|
+
constructor(stdin = process.stdin, stdout = process.stdout) {
|
|
838
|
+
super(stdin, stdout);
|
|
839
|
+
this.stdin = stdin;
|
|
840
|
+
this.handleStreamEnd = () => {
|
|
841
|
+
void this.ensureClosed().catch((error) => {
|
|
842
|
+
this.reportError(error);
|
|
843
|
+
});
|
|
844
|
+
};
|
|
845
|
+
this.handleStreamClose = () => {
|
|
846
|
+
void this.ensureClosed().catch((error) => {
|
|
847
|
+
this.reportError(error);
|
|
848
|
+
});
|
|
849
|
+
};
|
|
850
|
+
this.stdin.on('end', this.handleStreamEnd);
|
|
851
|
+
this.stdin.on('close', this.handleStreamClose);
|
|
852
|
+
}
|
|
853
|
+
ensureClosed() {
|
|
854
|
+
if (!this.closePromise) {
|
|
855
|
+
this.stdin.off('end', this.handleStreamEnd);
|
|
856
|
+
this.stdin.off('close', this.handleStreamClose);
|
|
857
|
+
this.closePromise = super.close();
|
|
858
|
+
}
|
|
859
|
+
return this.closePromise;
|
|
860
|
+
}
|
|
861
|
+
async close() {
|
|
862
|
+
await this.ensureClosed();
|
|
863
|
+
}
|
|
864
|
+
reportError(error) {
|
|
865
|
+
if (error instanceof Error) {
|
|
866
|
+
this.onerror?.(error);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
let message;
|
|
870
|
+
if (typeof error === 'string') {
|
|
871
|
+
message = error;
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
try {
|
|
875
|
+
message = JSON.stringify(error);
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
message = '[unknown error]';
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
this.onerror?.(new Error(message));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
export async function createSerenaStdioServer(factory, options = {}) {
|
|
885
|
+
const transport = new SerenaStdioServerTransport();
|
|
886
|
+
const mcpServer = factory.createMcpServer(options);
|
|
887
|
+
await mcpServer.connect(transport);
|
|
888
|
+
return {
|
|
889
|
+
transport,
|
|
890
|
+
mcpServer,
|
|
891
|
+
async close() {
|
|
892
|
+
await Promise.allSettled([transport.close(), mcpServer.close()]);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
export async function createSerenaGrpcServer(_factory, _options = {}) {
|
|
897
|
+
try {
|
|
898
|
+
await import('@grpc/grpc-js');
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
throw new Error('gRPCトランスポートPoCは未実装です。@grpc/grpc-js を導入後に createSerenaGrpcServer を拡張してください。', { cause: error });
|
|
902
|
+
}
|
|
903
|
+
throw new Error('gRPCトランスポートPoCは未実装です。HTTPモードのPoCは createSerenaHttpServer を利用してください。');
|
|
904
|
+
}
|