@ezmodo/mcp-server 0.13.4 → 0.13.5
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/http.js +5 -2
- package/lib/create-server.js +32 -3
- package/lib/remote-tools.js +151 -0
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/http.js
CHANGED
|
@@ -101,7 +101,7 @@ export function protectedResourceMetadata() {
|
|
|
101
101
|
* server without being told out of band.
|
|
102
102
|
*/
|
|
103
103
|
function authenticateChallenge(error, description) {
|
|
104
|
-
const parts = [
|
|
104
|
+
const parts = ['Bearer realm="ezmodo-mcp"', `resource_metadata="${METADATA_URL}"`];
|
|
105
105
|
if (error) parts.push(`error="${error}"`);
|
|
106
106
|
if (description) parts.push(`error_description="${description}"`);
|
|
107
107
|
return parts.join(', ');
|
|
@@ -169,7 +169,10 @@ async function handleMcpPost(req, res, requestId) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// One server and transport per request — see the stateless note at the top.
|
|
172
|
-
|
|
172
|
+
// 'remote': excludes tools that operate on a local checkout, which do not
|
|
173
|
+
// exist here and whose git helpers shell out with caller-supplied arguments
|
|
174
|
+
// (#2614).
|
|
175
|
+
const server = createServer({ surface: 'remote' });
|
|
173
176
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
174
177
|
|
|
175
178
|
// Closing on response end matters: without it every request leaks a transport
|
package/lib/create-server.js
CHANGED
|
@@ -25,21 +25,50 @@ import { HANDLERS } from '../handlers/index.js';
|
|
|
25
25
|
import { PROMPTS, getPromptContent } from '../prompts/index.js';
|
|
26
26
|
import { MCP_VERSION } from './version.js';
|
|
27
27
|
import { getLogger } from './logger.js';
|
|
28
|
+
import { isRemoteSafe } from './remote-tools.js';
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* @param {object} [options]
|
|
32
|
+
* @param {'local'|'remote'} [options.surface] Which tool surface to serve.
|
|
33
|
+
* 'local' (the default) is the full set, for stdio on a user's machine.
|
|
34
|
+
* 'remote' excludes tools that operate on the local filesystem or git — see
|
|
35
|
+
* lib/remote-tools.js for why that is an allowlist and not a denylist.
|
|
36
|
+
*/
|
|
37
|
+
export function createServer({ surface = 'local' } = {}) {
|
|
30
38
|
const log = getLogger();
|
|
31
39
|
|
|
40
|
+
// Filtered ONCE here rather than at each call site, so listing and dispatch
|
|
41
|
+
// cannot disagree. They must agree: filtering only tools/list would leave
|
|
42
|
+
// every excluded handler dispatchable by a client that guesses the name,
|
|
43
|
+
// which is the failure this whole module exists to prevent.
|
|
44
|
+
const tools = surface === 'remote' ? TOOLS.filter((tool) => isRemoteSafe(tool.name)) : TOOLS;
|
|
45
|
+
const available = new Set(tools.map((tool) => tool.name));
|
|
46
|
+
|
|
32
47
|
const server = new Server(
|
|
33
48
|
{ name: 'ezmodo-mcp-server', version: MCP_VERSION },
|
|
34
49
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
35
50
|
);
|
|
36
51
|
|
|
37
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools
|
|
52
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
38
53
|
|
|
39
54
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
40
55
|
const { name, arguments: args } = request.params;
|
|
41
|
-
const handler = HANDLERS[name];
|
|
42
56
|
|
|
57
|
+
// Checked before the handler lookup: a tool excluded from this surface is
|
|
58
|
+
// refused even though HANDLERS still contains it.
|
|
59
|
+
if (!available.has(name)) {
|
|
60
|
+
const handler = HANDLERS[name];
|
|
61
|
+
if (handler) {
|
|
62
|
+
log.warn('Refused a tool not served on this surface', { tool: name, surface });
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Tool "${name}" is not available over this connection. It operates on a local ` +
|
|
65
|
+
'checkout and is served only by the local (stdio) MCP server.'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handler = HANDLERS[name];
|
|
43
72
|
if (!handler) {
|
|
44
73
|
throw new Error(`Unknown tool: ${name}`);
|
|
45
74
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tools may be served over the REMOTE transport (#2614).
|
|
3
|
+
*
|
|
4
|
+
* The HTTP entry point and the stdio entry point deliberately build the same
|
|
5
|
+
* server (lib/create-server.js) so their tool surfaces cannot drift. That is
|
|
6
|
+
* right for almost everything — and wrong for the handful of tools that are
|
|
7
|
+
* not network operations at all.
|
|
8
|
+
*
|
|
9
|
+
* `detect_git_repository`, `manage_worktree`, `rebuild_manifest` and friends
|
|
10
|
+
* read and write the LOCAL machine: the working directory, `.ezmodo/`, the git
|
|
11
|
+
* repository. Over stdio that is the whole point — the server runs inside the
|
|
12
|
+
* user's checkout, at their request, as them. On a hosted server there is no
|
|
13
|
+
* checkout, so at best they fail confusingly.
|
|
14
|
+
*
|
|
15
|
+
* At worst they are dangerous. lib/git-helpers.js `execGit` passes its command
|
|
16
|
+
* to `execSync` — a shell — and callers interpolate tool arguments into it:
|
|
17
|
+
*
|
|
18
|
+
* execGit(`git branch ${branchName} ${baseBranch}`, repoPath)
|
|
19
|
+
*
|
|
20
|
+
* On a user's own machine the blast radius is their own shell, which is why
|
|
21
|
+
* this has been tolerable. Reachable over the internet it is arbitrary command
|
|
22
|
+
* execution in the container. So these tools are not hardened for remote use —
|
|
23
|
+
* they are not served remotely at all, because they have no meaning there and
|
|
24
|
+
* hardening would leave a shell-executing surface exposed for no benefit.
|
|
25
|
+
*
|
|
26
|
+
* ── The list is an ALLOWLIST, deliberately ───────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* A denylist would mean every tool added from now on is exposed remotely by
|
|
29
|
+
* default, and the mistake would be invisible: the tool simply works, until one
|
|
30
|
+
* of them turns out to touch the filesystem. Listing what is safe means a new
|
|
31
|
+
* tool is refused remotely until someone decides otherwise, and
|
|
32
|
+
* remote-tools.test.js fails loudly when a tool is unclassified rather than
|
|
33
|
+
* letting it through.
|
|
34
|
+
*
|
|
35
|
+
* Note some tools do local work as a SIDE EFFECT and are still fine here:
|
|
36
|
+
* manage_task writes `.ezmodo/active-session.json`, and lib/active-session.js
|
|
37
|
+
* already skips that when no config directory exists — which is exactly the
|
|
38
|
+
* case in a container. Likewise get_context and resolve_concepts read a local
|
|
39
|
+
* manifest when there is one and fall back to the API when there is not.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** Tools that operate on the local machine and are never served remotely. */
|
|
43
|
+
export const LOCAL_ONLY_TOOLS = Object.freeze([
|
|
44
|
+
'detect_git_repository',
|
|
45
|
+
'get_current_project_context',
|
|
46
|
+
'initialize_project_context',
|
|
47
|
+
'list_project_worktrees',
|
|
48
|
+
'manage_worktree',
|
|
49
|
+
'rebuild_manifest',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/** Tools safe to serve over the remote transport. */
|
|
53
|
+
export const REMOTE_SAFE_TOOLS = Object.freeze([
|
|
54
|
+
'accept_agent_suggestion',
|
|
55
|
+
'configure_agent',
|
|
56
|
+
'create_tasks',
|
|
57
|
+
'delete_attachment',
|
|
58
|
+
'estimate_task',
|
|
59
|
+
'evaluate_feature_flag',
|
|
60
|
+
'get_access',
|
|
61
|
+
'get_ai_insights',
|
|
62
|
+
'get_attachment_url',
|
|
63
|
+
'get_catalog',
|
|
64
|
+
'get_catalog_diff',
|
|
65
|
+
'get_context',
|
|
66
|
+
'get_decision',
|
|
67
|
+
'get_design',
|
|
68
|
+
'get_design_system',
|
|
69
|
+
'get_document',
|
|
70
|
+
'get_document_template',
|
|
71
|
+
'get_epic',
|
|
72
|
+
'get_feature',
|
|
73
|
+
'get_feature_flag',
|
|
74
|
+
'get_goal',
|
|
75
|
+
'get_graph',
|
|
76
|
+
'get_manifest_schema',
|
|
77
|
+
'get_milestone',
|
|
78
|
+
'get_org_areas',
|
|
79
|
+
'get_organization',
|
|
80
|
+
'get_project',
|
|
81
|
+
'get_project_changes',
|
|
82
|
+
'get_project_story',
|
|
83
|
+
'get_task',
|
|
84
|
+
'get_testing_summary',
|
|
85
|
+
'infer_dependencies',
|
|
86
|
+
'list_agent_suggestions',
|
|
87
|
+
'list_attachments',
|
|
88
|
+
'list_catalog_items',
|
|
89
|
+
'list_catalogs',
|
|
90
|
+
'list_components',
|
|
91
|
+
'list_designs',
|
|
92
|
+
'list_epics',
|
|
93
|
+
'list_facts',
|
|
94
|
+
'list_feature_flags',
|
|
95
|
+
'list_folders',
|
|
96
|
+
'list_links',
|
|
97
|
+
'list_notifications',
|
|
98
|
+
'list_org_documents',
|
|
99
|
+
'list_repositories',
|
|
100
|
+
'list_tags',
|
|
101
|
+
'list_test_cases',
|
|
102
|
+
'list_test_suites',
|
|
103
|
+
'list_todos',
|
|
104
|
+
'list_watched',
|
|
105
|
+
'manage_access',
|
|
106
|
+
'manage_catalog',
|
|
107
|
+
'manage_component',
|
|
108
|
+
'manage_decision',
|
|
109
|
+
'manage_design',
|
|
110
|
+
'manage_document',
|
|
111
|
+
'manage_document_template',
|
|
112
|
+
'manage_environment',
|
|
113
|
+
'manage_epic',
|
|
114
|
+
'manage_fact',
|
|
115
|
+
'manage_feature',
|
|
116
|
+
'manage_feature_flag',
|
|
117
|
+
'manage_folder',
|
|
118
|
+
'manage_goal',
|
|
119
|
+
'manage_link',
|
|
120
|
+
'manage_milestone',
|
|
121
|
+
'manage_project',
|
|
122
|
+
'manage_pull_request',
|
|
123
|
+
'manage_recurring_task',
|
|
124
|
+
'manage_tag',
|
|
125
|
+
'manage_task',
|
|
126
|
+
'manage_team',
|
|
127
|
+
'manage_test_case',
|
|
128
|
+
'manage_test_suite',
|
|
129
|
+
'manage_todo',
|
|
130
|
+
'manage_watch',
|
|
131
|
+
'manage_work_template',
|
|
132
|
+
'preview_links',
|
|
133
|
+
'reject_agent_suggestion',
|
|
134
|
+
'report_untracked_work',
|
|
135
|
+
'resolve_concepts',
|
|
136
|
+
'resolve_link_suggestions',
|
|
137
|
+
'resolve_links',
|
|
138
|
+
'run_agent_now',
|
|
139
|
+
'search_epics',
|
|
140
|
+
'search_features',
|
|
141
|
+
'search_tasks',
|
|
142
|
+
'update_manifest_entries',
|
|
143
|
+
'validate_manifest',
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const remoteSafe = new Set(REMOTE_SAFE_TOOLS);
|
|
147
|
+
|
|
148
|
+
/** Whether a tool may be served over the remote transport. */
|
|
149
|
+
export function isRemoteSafe(toolName) {
|
|
150
|
+
return remoteSafe.has(toolName);
|
|
151
|
+
}
|
package/lib/version.js
CHANGED