cf-mcp 0.9.2
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/Manifest.txt +34 -0
- data/README.md +52 -0
- data/Rakefile +53 -0
- data/config.ru +26 -0
- data/exe/cf-mcp +6 -0
- data/lib/cf/mcp/cli.rb +211 -0
- data/lib/cf/mcp/downloader.rb +112 -0
- data/lib/cf/mcp/index.rb +128 -0
- data/lib/cf/mcp/models/doc_item.rb +171 -0
- data/lib/cf/mcp/models/enum_doc.rb +83 -0
- data/lib/cf/mcp/models/function_doc.rb +110 -0
- data/lib/cf/mcp/models/struct_doc.rb +83 -0
- data/lib/cf/mcp/models/topic_doc.rb +113 -0
- data/lib/cf/mcp/parser.rb +246 -0
- data/lib/cf/mcp/server.rb +316 -0
- data/lib/cf/mcp/templates/index.erb +94 -0
- data/lib/cf/mcp/templates/script.js +292 -0
- data/lib/cf/mcp/templates/style.css +165 -0
- data/lib/cf/mcp/tools/find_related.rb +77 -0
- data/lib/cf/mcp/tools/get_details.rb +64 -0
- data/lib/cf/mcp/tools/get_topic.rb +53 -0
- data/lib/cf/mcp/tools/list_category.rb +77 -0
- data/lib/cf/mcp/tools/list_topics.rb +64 -0
- data/lib/cf/mcp/tools/member_search.rb +76 -0
- data/lib/cf/mcp/tools/parameter_search.rb +102 -0
- data/lib/cf/mcp/tools/search_enums.rb +57 -0
- data/lib/cf/mcp/tools/search_functions.rb +57 -0
- data/lib/cf/mcp/tools/search_structs.rb +57 -0
- data/lib/cf/mcp/tools/search_tool.rb +58 -0
- data/lib/cf/mcp/topic_parser.rb +199 -0
- data/lib/cf/mcp/version.rb +7 -0
- data/lib/cf/mcp.rb +23 -0
- data/sig/cf/mcp.rbs +84 -0
- metadata +150 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { h, render } from 'https://esm.sh/preact@10';
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'https://esm.sh/preact@10/hooks';
|
|
3
|
+
import htm from 'https://esm.sh/htm@3';
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
const TOOLS = TOOL_SCHEMAS_PLACEHOLDER;
|
|
8
|
+
const CATEGORIES = CATEGORIES_PLACEHOLDER;
|
|
9
|
+
const TOPICS = TOPICS_PLACEHOLDER;
|
|
10
|
+
const CHANGELOG = CHANGELOG_PLACEHOLDER;
|
|
11
|
+
|
|
12
|
+
// Helper to render topic options (used in multiple places)
|
|
13
|
+
const renderTopicOptions = () =>
|
|
14
|
+
TOPICS.map(topic => html`<option value=${topic.name}>${topic.name.replace(/_/g, ' ')}</option>`);
|
|
15
|
+
|
|
16
|
+
// Reusable select field component
|
|
17
|
+
function SelectField({ id, label, value, onChange, required, placeholder, options }) {
|
|
18
|
+
return html`
|
|
19
|
+
<div class="form-group">
|
|
20
|
+
<label class="form-label" for=${id}>${label}</label>
|
|
21
|
+
<select id=${id} class="form-select" value=${value} onChange=${onChange} required=${required}>
|
|
22
|
+
<option value="">${placeholder}</option>
|
|
23
|
+
${options}
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Custom hook for MCP tool calls
|
|
30
|
+
function useMcpToolCall() {
|
|
31
|
+
const [result, setResult] = useState({ loading: false, error: null, content: null });
|
|
32
|
+
|
|
33
|
+
const callTool = useCallback(async (toolName, args = {}, loadingText = 'Loading...') => {
|
|
34
|
+
setResult({ loading: true, loadingText, error: null, content: null });
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch('/http', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'Accept': 'application/json, text/event-stream'
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
jsonrpc: '2.0',
|
|
45
|
+
id: Date.now(),
|
|
46
|
+
method: 'tools/call',
|
|
47
|
+
params: { name: toolName, arguments: args }
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
|
|
53
|
+
if (data.error) {
|
|
54
|
+
setResult({ loading: false, error: 'Error: ' + (data.error.message || JSON.stringify(data.error)), content: null });
|
|
55
|
+
} else if (data.result?.content) {
|
|
56
|
+
const content = data.result.content[0];
|
|
57
|
+
const text = content.text || JSON.stringify(content);
|
|
58
|
+
setResult({
|
|
59
|
+
loading: false,
|
|
60
|
+
error: data.result.isError ? text : null,
|
|
61
|
+
content: data.result.isError ? null : text
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
setResult({ loading: false, error: null, content: JSON.stringify(data, null, 2) });
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
setResult({ loading: false, error: 'Network error: ' + err.message, content: null });
|
|
68
|
+
}
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const reset = useCallback(() => {
|
|
72
|
+
setResult({ loading: false, error: null, content: null });
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
return { result, callTool, reset };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Shared ResultArea component
|
|
79
|
+
function ResultArea({ result }) {
|
|
80
|
+
const containerRef = useRef(null);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (result.content && containerRef.current) {
|
|
84
|
+
containerRef.current.innerHTML = marked.parse(result.content);
|
|
85
|
+
convertListsToDefinitionLists(containerRef.current);
|
|
86
|
+
}
|
|
87
|
+
}, [result.content]);
|
|
88
|
+
|
|
89
|
+
if (result.loading) {
|
|
90
|
+
return html`<div class="result-area visible"><span class="loading">${result.loadingText || 'Loading...'}</span></div>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (result.error) {
|
|
94
|
+
return html`<div class="result-area visible error">${result.error}</div>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (result.content) {
|
|
98
|
+
return html`<div class="result-area visible success" ref=${containerRef}></div>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return html`<div class="result-area"></div>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function convertListsToDefinitionLists(container) {
|
|
105
|
+
const lists = container.querySelectorAll('ul');
|
|
106
|
+
lists.forEach(ul => {
|
|
107
|
+
const items = ul.querySelectorAll('li');
|
|
108
|
+
const isDefinitionList = Array.from(items).every(li => li.innerHTML.includes(' — '));
|
|
109
|
+
if (!isDefinitionList || items.length === 0) return;
|
|
110
|
+
|
|
111
|
+
const dl = document.createElement('dl');
|
|
112
|
+
items.forEach(li => {
|
|
113
|
+
const parts = li.innerHTML.split(' — ');
|
|
114
|
+
if (parts.length >= 2) {
|
|
115
|
+
const dt = document.createElement('dt');
|
|
116
|
+
dt.innerHTML = parts[0];
|
|
117
|
+
const dd = document.createElement('dd');
|
|
118
|
+
dd.innerHTML = parts.slice(1).join(' — ');
|
|
119
|
+
dl.appendChild(dt);
|
|
120
|
+
dl.appendChild(dd);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
ul.replaceWith(dl);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Helper to create a select input
|
|
128
|
+
function renderSelect(id, value, onChange, required, placeholder, options) {
|
|
129
|
+
return html`
|
|
130
|
+
<select id=${id} class="form-select" value=${value} onChange=${onChange} required=${required}>
|
|
131
|
+
<option value="">${placeholder}</option>
|
|
132
|
+
${options}
|
|
133
|
+
</select>
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Form field component
|
|
138
|
+
function FormField({ name, propDef, required, value, onChange, toolName }) {
|
|
139
|
+
const id = `field-${name}`;
|
|
140
|
+
const description = propDef.description || '';
|
|
141
|
+
const handleChange = e => onChange(name, e.target.value);
|
|
142
|
+
|
|
143
|
+
let input;
|
|
144
|
+
|
|
145
|
+
if (propDef.enum) {
|
|
146
|
+
input = renderSelect(id, value, handleChange, required, '-- Select --',
|
|
147
|
+
propDef.enum.map(val => html`<option value=${val}>${val}</option>`));
|
|
148
|
+
} else if (propDef.type === 'integer') {
|
|
149
|
+
const attrs = name === 'limit' ? { min: 1, max: 20 } : {};
|
|
150
|
+
input = html`
|
|
151
|
+
<input
|
|
152
|
+
type="number"
|
|
153
|
+
id=${id}
|
|
154
|
+
class="form-input"
|
|
155
|
+
placeholder=${description}
|
|
156
|
+
value=${value}
|
|
157
|
+
onChange=${e => onChange(name, e.target.value ? parseInt(e.target.value, 10) : '')}
|
|
158
|
+
required=${required}
|
|
159
|
+
...${attrs}
|
|
160
|
+
/>
|
|
161
|
+
`;
|
|
162
|
+
} else if (name === 'category') {
|
|
163
|
+
input = renderSelect(id, value, handleChange, required, '-- All Categories --',
|
|
164
|
+
CATEGORIES.map(cat => html`<option value=${cat}>${cat}</option>`));
|
|
165
|
+
} else if (name === 'name' && toolName === 'cf_get_topic') {
|
|
166
|
+
input = renderSelect(id, value, handleChange, required, '-- Select Topic --', renderTopicOptions());
|
|
167
|
+
} else {
|
|
168
|
+
input = html`
|
|
169
|
+
<input
|
|
170
|
+
type="text"
|
|
171
|
+
id=${id}
|
|
172
|
+
class="form-input"
|
|
173
|
+
placeholder=${description}
|
|
174
|
+
value=${value}
|
|
175
|
+
onInput=${e => onChange(name, e.target.value)}
|
|
176
|
+
required=${required}
|
|
177
|
+
/>
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return html`
|
|
182
|
+
<div class="form-group">
|
|
183
|
+
<label class="form-label" for=${id}>
|
|
184
|
+
${name} ${required && html`<span class="required">*</span>`}
|
|
185
|
+
</label>
|
|
186
|
+
${input}
|
|
187
|
+
${description && html`<div class="form-hint">${description}</div>`}
|
|
188
|
+
</div>
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Tool Explorer component
|
|
193
|
+
function ToolExplorer() {
|
|
194
|
+
const [selectedTool, setSelectedTool] = useState(TOOLS[0]?.name || '');
|
|
195
|
+
const [formValues, setFormValues] = useState({ limit: 20 });
|
|
196
|
+
const { result, callTool, reset } = useMcpToolCall();
|
|
197
|
+
|
|
198
|
+
const tool = TOOLS.find(t => t.name === selectedTool);
|
|
199
|
+
const schema = tool?.inputSchema || {};
|
|
200
|
+
const properties = schema.properties || {};
|
|
201
|
+
const required = schema.required || [];
|
|
202
|
+
|
|
203
|
+
const handleFieldChange = (name, value) => {
|
|
204
|
+
setFormValues(prev => ({ ...prev, [name]: value }));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const handleToolChange = (e) => {
|
|
208
|
+
setSelectedTool(e.target.value);
|
|
209
|
+
reset();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const handleSubmit = async (e) => {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
|
|
215
|
+
const args = {};
|
|
216
|
+
for (const [name, value] of Object.entries(formValues)) {
|
|
217
|
+
if (value !== '' && value !== undefined && properties[name]) {
|
|
218
|
+
args[name] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
callTool(selectedTool, args, 'Executing...');
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return html`
|
|
226
|
+
<div class="explorer">
|
|
227
|
+
<form onSubmit=${handleSubmit}>
|
|
228
|
+
<${SelectField}
|
|
229
|
+
id="tool-select"
|
|
230
|
+
label="Select Tool"
|
|
231
|
+
value=${selectedTool}
|
|
232
|
+
onChange=${handleToolChange}
|
|
233
|
+
placeholder="-- Select Tool --"
|
|
234
|
+
options=${TOOLS.map(t => html`<option value=${t.name}>${t.name}</option>`)}
|
|
235
|
+
/>
|
|
236
|
+
${Object.entries(properties).map(([propName, propDef]) => html`
|
|
237
|
+
<${FormField}
|
|
238
|
+
key=${propName}
|
|
239
|
+
name=${propName}
|
|
240
|
+
propDef=${propDef}
|
|
241
|
+
required=${required.includes(propName)}
|
|
242
|
+
value=${formValues[propName] ?? (propName === 'limit' ? 20 : '')}
|
|
243
|
+
onChange=${handleFieldChange}
|
|
244
|
+
toolName=${selectedTool}
|
|
245
|
+
/>
|
|
246
|
+
`)}
|
|
247
|
+
<button type="submit" class="btn-execute" disabled=${result.loading}>Execute</button>
|
|
248
|
+
</form>
|
|
249
|
+
<${ResultArea} result=${result} />
|
|
250
|
+
</div>
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Topics Explorer component
|
|
255
|
+
function TopicsExplorer() {
|
|
256
|
+
const [selectedTopic, setSelectedTopic] = useState('');
|
|
257
|
+
const { result, callTool, reset } = useMcpToolCall();
|
|
258
|
+
|
|
259
|
+
const handleChange = (e) => {
|
|
260
|
+
const value = e.target.value;
|
|
261
|
+
setSelectedTopic(value);
|
|
262
|
+
if (value) {
|
|
263
|
+
callTool('cf_get_topic', { name: value }, 'Loading topic...');
|
|
264
|
+
} else {
|
|
265
|
+
reset();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return html`
|
|
270
|
+
<div class="explorer">
|
|
271
|
+
<${SelectField}
|
|
272
|
+
id="topic-select"
|
|
273
|
+
label="Select Topic"
|
|
274
|
+
value=${selectedTopic}
|
|
275
|
+
onChange=${handleChange}
|
|
276
|
+
placeholder="-- Select a topic --"
|
|
277
|
+
options=${renderTopicOptions()}
|
|
278
|
+
/>
|
|
279
|
+
<${ResultArea} result=${result} />
|
|
280
|
+
</div>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Mount components
|
|
285
|
+
render(html`<${ToolExplorer} />`, document.getElementById('tool-explorer-root'));
|
|
286
|
+
render(html`<${TopicsExplorer} />`, document.getElementById('topics-explorer-root'));
|
|
287
|
+
|
|
288
|
+
// Render changelog
|
|
289
|
+
const changelogEl = document.getElementById('changelog-content');
|
|
290
|
+
if (changelogEl && CHANGELOG) {
|
|
291
|
+
changelogEl.innerHTML = marked.parse(CHANGELOG);
|
|
292
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
* { box-sizing: border-box; }
|
|
2
|
+
body {
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
4
|
+
line-height: 1.6;
|
|
5
|
+
max-width: 800px;
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
padding: 2rem;
|
|
8
|
+
background: #0d1117;
|
|
9
|
+
color: #c9d1d9;
|
|
10
|
+
}
|
|
11
|
+
h1 { color: #58a6ff; margin-bottom: 0.5rem; }
|
|
12
|
+
h2 { color: #58a6ff; margin-top: 2rem; border-bottom: 1px solid #30363d; padding-bottom: 0.5rem; }
|
|
13
|
+
a { color: #58a6ff; }
|
|
14
|
+
code {
|
|
15
|
+
background: #161b22;
|
|
16
|
+
padding: 0.2rem 0.4rem;
|
|
17
|
+
border-radius: 4px;
|
|
18
|
+
font-size: 0.9em;
|
|
19
|
+
}
|
|
20
|
+
pre {
|
|
21
|
+
background: #161b22;
|
|
22
|
+
padding: 1rem;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
overflow-x: auto;
|
|
25
|
+
border: 1px solid #30363d;
|
|
26
|
+
}
|
|
27
|
+
pre code { background: none; padding: 0; }
|
|
28
|
+
.stats {
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
31
|
+
gap: 1rem;
|
|
32
|
+
margin: 1rem 0;
|
|
33
|
+
}
|
|
34
|
+
.stat {
|
|
35
|
+
background: #161b22;
|
|
36
|
+
padding: 1rem;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
text-align: center;
|
|
39
|
+
border: 1px solid #30363d;
|
|
40
|
+
}
|
|
41
|
+
.stat-value { font-size: 2rem; font-weight: bold; color: #58a6ff; }
|
|
42
|
+
.stat-label { color: #8b949e; font-size: 0.9rem; }
|
|
43
|
+
.endpoint {
|
|
44
|
+
background: #161b22;
|
|
45
|
+
padding: 1rem;
|
|
46
|
+
border-radius: 8px;
|
|
47
|
+
margin: 0.5rem 0;
|
|
48
|
+
border: 1px solid #30363d;
|
|
49
|
+
}
|
|
50
|
+
.endpoint-path { font-weight: bold; color: #7ee787; }
|
|
51
|
+
.endpoint-desc { color: #8b949e; margin-top: 0.25rem; }
|
|
52
|
+
.tools { margin: 1rem 0; }
|
|
53
|
+
.tool {
|
|
54
|
+
background: #161b22;
|
|
55
|
+
padding: 0.75rem 1rem;
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
margin: 0.5rem 0;
|
|
58
|
+
border: 1px solid #30363d;
|
|
59
|
+
}
|
|
60
|
+
.tool-name { font-weight: bold; color: #ffa657; }
|
|
61
|
+
.tool-desc { color: #8b949e; font-size: 0.9rem; }
|
|
62
|
+
.explorer {
|
|
63
|
+
background: #161b22;
|
|
64
|
+
border: 1px solid #30363d;
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
padding: 1.5rem;
|
|
67
|
+
margin: 1rem 0;
|
|
68
|
+
}
|
|
69
|
+
.form-group {
|
|
70
|
+
margin-bottom: 1rem;
|
|
71
|
+
}
|
|
72
|
+
.form-label {
|
|
73
|
+
display: block;
|
|
74
|
+
margin-bottom: 0.25rem;
|
|
75
|
+
color: #c9d1d9;
|
|
76
|
+
font-size: 0.9rem;
|
|
77
|
+
}
|
|
78
|
+
.form-label .required {
|
|
79
|
+
color: #f85149;
|
|
80
|
+
}
|
|
81
|
+
.form-input, .form-select {
|
|
82
|
+
width: 100%;
|
|
83
|
+
padding: 0.5rem 0.75rem;
|
|
84
|
+
background: #0d1117;
|
|
85
|
+
border: 1px solid #30363d;
|
|
86
|
+
border-radius: 6px;
|
|
87
|
+
color: #c9d1d9;
|
|
88
|
+
font-size: 0.9rem;
|
|
89
|
+
}
|
|
90
|
+
.form-input:focus, .form-select:focus {
|
|
91
|
+
outline: none;
|
|
92
|
+
border-color: #58a6ff;
|
|
93
|
+
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.3);
|
|
94
|
+
}
|
|
95
|
+
.form-input::placeholder {
|
|
96
|
+
color: #6e7681;
|
|
97
|
+
}
|
|
98
|
+
.form-hint {
|
|
99
|
+
font-size: 0.8rem;
|
|
100
|
+
color: #6e7681;
|
|
101
|
+
margin-top: 0.25rem;
|
|
102
|
+
}
|
|
103
|
+
.btn-execute {
|
|
104
|
+
background: #238636;
|
|
105
|
+
color: #fff;
|
|
106
|
+
border: none;
|
|
107
|
+
padding: 0.75rem 1.5rem;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
font-size: 1rem;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
}
|
|
113
|
+
.btn-execute:hover {
|
|
114
|
+
background: #2ea043;
|
|
115
|
+
}
|
|
116
|
+
.btn-execute:disabled {
|
|
117
|
+
background: #21262d;
|
|
118
|
+
color: #6e7681;
|
|
119
|
+
cursor: not-allowed;
|
|
120
|
+
}
|
|
121
|
+
.result-area {
|
|
122
|
+
background: #0d1117;
|
|
123
|
+
border: 1px solid #30363d;
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
padding: 1rem;
|
|
126
|
+
margin-top: 1rem;
|
|
127
|
+
word-wrap: break-word;
|
|
128
|
+
font-size: 0.9rem;
|
|
129
|
+
max-height: 400px;
|
|
130
|
+
overflow-y: auto;
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
.result-area.visible {
|
|
134
|
+
display: block;
|
|
135
|
+
}
|
|
136
|
+
.result-area.error {
|
|
137
|
+
border-color: #f85149;
|
|
138
|
+
color: #f85149;
|
|
139
|
+
}
|
|
140
|
+
.result-area.success {
|
|
141
|
+
border-color: #238636;
|
|
142
|
+
}
|
|
143
|
+
.loading {
|
|
144
|
+
color: #6e7681;
|
|
145
|
+
font-style: italic;
|
|
146
|
+
}
|
|
147
|
+
/* Markdown content styles */
|
|
148
|
+
.result-area h1 { font-size: 1.4em; color: #58a6ff; margin: 0 0 0.5em 0; border-bottom: 1px solid #30363d; padding-bottom: 0.3em; }
|
|
149
|
+
.result-area h2 { font-size: 1.2em; color: #58a6ff; margin: 1em 0 0.5em 0; }
|
|
150
|
+
.result-area h3 { font-size: 1.1em; color: #58a6ff; margin: 1em 0 0.5em 0; }
|
|
151
|
+
.result-area p { margin: 0.5em 0; }
|
|
152
|
+
.result-area strong { color: #ffa657; }
|
|
153
|
+
.result-area code { background: #21262d; padding: 0.15em 0.3em; border-radius: 3px; font-size: 0.9em; }
|
|
154
|
+
.result-area pre { background: #21262d; padding: 0.75em; border-radius: 6px; overflow-x: auto; margin: 0.5em 0; }
|
|
155
|
+
.result-area pre code { background: none; padding: 0; }
|
|
156
|
+
.result-area table { border-collapse: collapse; width: 100%; margin: 0.5em 0; }
|
|
157
|
+
.result-area th, .result-area td { border: 1px solid #30363d; padding: 0.4em 0.6em; text-align: left; }
|
|
158
|
+
.result-area th { background: #21262d; color: #58a6ff; }
|
|
159
|
+
.result-area ul, .result-area ol { margin: 0.5em 0; padding-left: 1.5em; }
|
|
160
|
+
.result-area li { margin: 0.25em 0; }
|
|
161
|
+
.result-area a { color: #58a6ff; }
|
|
162
|
+
.result-area dl { margin: 0.5em 0; }
|
|
163
|
+
.result-area dt { color: #c9d1d9; margin-top: 0.75em; padding-bottom: 0.25em; border-bottom: 1px solid #21262d; }
|
|
164
|
+
.result-area dt:first-child { margin-top: 0; }
|
|
165
|
+
.result-area dd { margin: 0.25em 0 0 1em; color: #8b949e; }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class FindRelated < ::MCP::Tool
|
|
9
|
+
tool_name "cf_find_related"
|
|
10
|
+
description "Find all items related to a given Cute Framework item (bidirectional relationship search)"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
name: {type: "string", description: "Name of the item to find relations for (e.g., 'CF_Sprite', 'cf_make_sprite')"}
|
|
16
|
+
},
|
|
17
|
+
required: ["name"]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def self.call(name:, server_context: {})
|
|
21
|
+
index = server_context[:index]
|
|
22
|
+
return error_response("Index not available") unless index
|
|
23
|
+
|
|
24
|
+
item = index.find(name)
|
|
25
|
+
return text_response("Not found: '#{name}'") unless item
|
|
26
|
+
|
|
27
|
+
# Forward references: items this item references
|
|
28
|
+
forward_refs = (item.related || []).map { |ref_name|
|
|
29
|
+
ref_item = index.find(ref_name)
|
|
30
|
+
if ref_item
|
|
31
|
+
"- `#{ref_item.name}` (#{ref_item.type}) — #{ref_item.brief}"
|
|
32
|
+
else
|
|
33
|
+
"- `#{ref_name}` (not found in index)"
|
|
34
|
+
end
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Back references: items that reference this item
|
|
38
|
+
back_refs = []
|
|
39
|
+
index.items.each_value do |other_item|
|
|
40
|
+
next if other_item.name == name
|
|
41
|
+
next unless other_item.related&.include?(name)
|
|
42
|
+
|
|
43
|
+
back_refs << "- `#{other_item.name}` (#{other_item.type}) — #{other_item.brief}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if forward_refs.empty? && back_refs.empty?
|
|
47
|
+
return text_response("# #{name}\n\nNo related items found.\n\n**Tip:** Not all items have explicit relationships documented.")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
lines = ["# Related items for #{name}", ""]
|
|
51
|
+
|
|
52
|
+
unless forward_refs.empty?
|
|
53
|
+
lines << "## References (items this references)"
|
|
54
|
+
lines.concat(forward_refs)
|
|
55
|
+
lines << ""
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
unless back_refs.empty?
|
|
59
|
+
lines << "## Referenced by (items that reference this)"
|
|
60
|
+
lines.concat(back_refs)
|
|
61
|
+
lines << ""
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
text_response(lines.join("\n"))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.text_response(text)
|
|
68
|
+
::MCP::Tool::Response.new([{type: "text", text: text}])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.error_response(message)
|
|
72
|
+
::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class GetDetails < ::MCP::Tool
|
|
9
|
+
tool_name "cf_get_details"
|
|
10
|
+
description "Get detailed documentation for a specific Cute Framework item by exact name"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
name: {type: "string", description: "Exact name of the item (e.g., 'cf_make_app', 'CF_Sprite', 'CF_PlayDirection')"}
|
|
16
|
+
},
|
|
17
|
+
required: ["name"]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
NAMING_TIP = "**Tip:** Cute Framework uses `cf_` prefix for functions and `CF_` prefix for types (structs/enums)."
|
|
21
|
+
|
|
22
|
+
def self.call(name:, server_context: {})
|
|
23
|
+
index = server_context[:index]
|
|
24
|
+
return error_response("Index not available") unless index
|
|
25
|
+
|
|
26
|
+
item = index.find(name)
|
|
27
|
+
|
|
28
|
+
if item.nil?
|
|
29
|
+
# Try a fuzzy search to suggest alternatives
|
|
30
|
+
suggestions = index.search(name, limit: 5)
|
|
31
|
+
if suggestions.empty?
|
|
32
|
+
text_response("Not found: '#{name}'\n\n#{NAMING_TIP}")
|
|
33
|
+
else
|
|
34
|
+
formatted = suggestions.map { |s| "- `#{s.name}` (#{s.type}) — #{s.brief}" }.join("\n")
|
|
35
|
+
text_response("Not found: '#{name}'\n\n**Similar items:**\n#{formatted}\n\n#{NAMING_TIP}")
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
output = item.to_text(detailed: true, index: index)
|
|
39
|
+
|
|
40
|
+
# Append related topics section for API items
|
|
41
|
+
if item.type != :topic
|
|
42
|
+
related_topics = index.topics_for(name)
|
|
43
|
+
if related_topics.any?
|
|
44
|
+
output += "\n\n## Related Topics\n"
|
|
45
|
+
output += related_topics.map { |t| "- **#{t.name}** — #{t.brief}" }.join("\n")
|
|
46
|
+
output += "\n\n**Tip:** Use `cf_get_topic` to read the full topic content."
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
text_response(output)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.text_response(text)
|
|
55
|
+
::MCP::Tool::Response.new([{type: "text", text: text}])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.error_response(message)
|
|
59
|
+
::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
|
|
5
|
+
module CF
|
|
6
|
+
module MCP
|
|
7
|
+
module Tools
|
|
8
|
+
class GetTopic < ::MCP::Tool
|
|
9
|
+
tool_name "cf_get_topic"
|
|
10
|
+
description "Get the full content of a Cute Framework topic guide document"
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
name: {type: "string", description: "Topic name (e.g., 'audio', 'collision', 'drawing', 'coroutines')"}
|
|
16
|
+
},
|
|
17
|
+
required: ["name"]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def self.call(name:, server_context: {})
|
|
21
|
+
index = server_context[:index]
|
|
22
|
+
return error_response("Index not available") unless index
|
|
23
|
+
|
|
24
|
+
topic = index.find(name)
|
|
25
|
+
|
|
26
|
+
if topic.nil? || topic.type != :topic
|
|
27
|
+
# Try fuzzy match on topic names
|
|
28
|
+
suggestions = index.topics.select { |t|
|
|
29
|
+
t.name.include?(name) || name.include?(t.name) || t.name.delete("_").include?(name.delete("_"))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if suggestions.empty?
|
|
33
|
+
text_response("Topic not found: '#{name}'\n\nUse `cf_list_topics` to see available topics.")
|
|
34
|
+
else
|
|
35
|
+
formatted = suggestions.map { |t| "- **#{t.name}** — #{t.brief}" }.join("\n")
|
|
36
|
+
text_response("Topic not found: '#{name}'\n\n**Similar topics:**\n#{formatted}")
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
text_response(topic.to_text(detailed: true, index: index))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.text_response(text)
|
|
44
|
+
::MCP::Tool::Response.new([{type: "text", text: text}])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.error_response(message)
|
|
48
|
+
::MCP::Tool::Response.new([{type: "text", text: "Error: #{message}"}], error: true)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|