fa-mcp-sdk 0.4.100 → 0.4.101
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/cli-template/.claude/skills/mcp-app-add-to-server/SKILL.md +9 -3
- package/cli-template/.claude/skills/mcp-app-create/SKILL.md +9 -3
- package/cli-template/CLAUDE.md +17 -0
- package/cli-template/gitignore +1 -0
- package/cli-template/package.json +1 -1
- package/package.json +1 -1
- package/scripts/clone-mcp-ext-apps.js +327 -0
|
@@ -13,13 +13,19 @@ Existing tools get paired with HTML resources that render inline in the host's c
|
|
|
13
13
|
|
|
14
14
|
## Getting Reference Code
|
|
15
15
|
|
|
16
|
-
Clone the MCP Apps SDK repository (`@modelcontextprotocol/ext-apps`)
|
|
16
|
+
Clone or update the MCP Apps SDK repository (`@modelcontextprotocol/ext-apps`) using the bundled
|
|
17
|
+
helper. The folder `./mcp-ext-apps/` is already in `.gitignore` and is intentionally persistent —
|
|
18
|
+
it serves as the long-lived reference checkout that this skill (and the `mcp-app-create` skill)
|
|
19
|
+
read from. Do not delete it after use.
|
|
17
20
|
|
|
18
21
|
```bash
|
|
19
|
-
|
|
22
|
+
node scripts/clone-mcp-ext-apps.js --tag latest
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
The script clones into `./mcp-ext-apps/` on first run, pulls the default branch on subsequent
|
|
26
|
+
runs, and (with `--tag latest`) checks out the latest released npm tag so the cloned tree
|
|
27
|
+
matches the published `@modelcontextprotocol/ext-apps` version. Add `--json` to capture machine-
|
|
28
|
+
readable metadata (path, ref, commit) for downstream automation.
|
|
23
29
|
|
|
24
30
|
### Protocol Specification
|
|
25
31
|
|
|
@@ -42,13 +42,19 @@ Host calls tool → Host renders resource UI → Server returns result → UI re
|
|
|
42
42
|
|
|
43
43
|
## Getting Reference Code
|
|
44
44
|
|
|
45
|
-
Clone the MCP Apps SDK repository (`@modelcontextprotocol/ext-apps`)
|
|
45
|
+
Clone or update the MCP Apps SDK repository (`@modelcontextprotocol/ext-apps`) using the bundled
|
|
46
|
+
helper. The folder `./mcp-ext-apps/` is already in `.gitignore` and is intentionally persistent —
|
|
47
|
+
it serves as the long-lived reference checkout that this skill (and the `mcp-app-add-to-server`
|
|
48
|
+
skill) read from. Do not delete it after use.
|
|
46
49
|
|
|
47
50
|
```bash
|
|
48
|
-
|
|
51
|
+
node scripts/clone-mcp-ext-apps.js --tag latest
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
The script clones into `./mcp-ext-apps/` on first run, pulls the default branch on subsequent
|
|
55
|
+
runs, and (with `--tag latest`) checks out the latest released npm tag so the cloned tree
|
|
56
|
+
matches the published `@modelcontextprotocol/ext-apps` version. Add `--json` to capture machine-
|
|
57
|
+
readable metadata (path, ref, commit) for downstream automation.
|
|
52
58
|
|
|
53
59
|
### Protocol Specification
|
|
54
60
|
|
package/cli-template/CLAUDE.md
CHANGED
|
@@ -357,6 +357,23 @@ Any edit or new file under `.claude/**` (SKILL.md, scripts, hooks, agents, `sett
|
|
|
357
357
|
by `settings.json` — direct `Write`/`Edit` will fail. Invoke the `/edit-claude-files` skill, which
|
|
358
358
|
describes the required `scripts/fcp.js` temp-copy protocol.
|
|
359
359
|
|
|
360
|
+
## MCP Apps Reference Clone (`scripts/clone-mcp-ext-apps.js`)
|
|
361
|
+
|
|
362
|
+
Shared helper used by the `/mcp-app-create` and `/mcp-app-add-to-server` skills. Clones or refreshes
|
|
363
|
+
`https://github.com/modelcontextprotocol/ext-apps.git` into `./mcp-ext-apps/` at the project root
|
|
364
|
+
(already in `.gitignore`, intentionally persistent so the same checkout is reused across runs).
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
node scripts/clone-mcp-ext-apps.js # clone on first run, pull main otherwise
|
|
368
|
+
node scripts/clone-mcp-ext-apps.js --tag latest # also checkout the latest npm tag
|
|
369
|
+
node scripts/clone-mcp-ext-apps.js --tag v1.7.2 # checkout a specific tag
|
|
370
|
+
node scripts/clone-mcp-ext-apps.js --json # JSON output (path, ref, commit, version)
|
|
371
|
+
node scripts/clone-mcp-ext-apps.js --list-examples # include examples/* metadata in JSON
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
The script never deletes `mcp-ext-apps/`. The two skills above call it before reading sources from
|
|
375
|
+
the cloned tree, so make sure it has run successfully before troubleshooting their behavior.
|
|
376
|
+
|
|
360
377
|
|
|
361
378
|
## Formatting
|
|
362
379
|
|
package/cli-template/gitignore
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fa-mcp-sdk",
|
|
3
3
|
"productName": "FA MCP SDK",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.101",
|
|
5
5
|
"description": "Core infrastructure and templates for building Model Context Protocol (MCP) servers with TypeScript",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/core/index.js",
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Clone or update the @modelcontextprotocol/ext-apps reference repository into
|
|
4
|
+
* mcp-ext-apps/ at the project root.
|
|
5
|
+
*
|
|
6
|
+
* The folder is intentionally persistent — it is listed in .gitignore but kept on
|
|
7
|
+
* disk so subsequent skill runs can read the cloned sources without re-cloning.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/clone-mcp-ext-apps.js # clone if missing, otherwise pull main
|
|
11
|
+
* node scripts/clone-mcp-ext-apps.js --tag latest # also checkout latest released npm tag
|
|
12
|
+
* node scripts/clone-mcp-ext-apps.js --tag v0.7.1 # checkout a specific tag
|
|
13
|
+
* node scripts/clone-mcp-ext-apps.js --json # emit JSON metadata to stdout
|
|
14
|
+
* node scripts/clone-mcp-ext-apps.js --list-examples # include examples/* metadata in output
|
|
15
|
+
*
|
|
16
|
+
* The script never deletes mcp-ext-apps/. Failed clones leave whatever git managed
|
|
17
|
+
* to write on disk; rerun the script to recover.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { exec } from 'child_process';
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
22
|
+
import { dirname, join, resolve } from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { promisify } from 'util';
|
|
25
|
+
|
|
26
|
+
const execAsync = promisify(exec);
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
29
|
+
const TARGET_DIR = join(PROJECT_ROOT, 'mcp-ext-apps');
|
|
30
|
+
const REPO_URL = 'https://github.com/modelcontextprotocol/ext-apps.git';
|
|
31
|
+
const PACKAGE_NAME = '@modelcontextprotocol/ext-apps';
|
|
32
|
+
|
|
33
|
+
const args = process.argv.slice(2);
|
|
34
|
+
const flags = {
|
|
35
|
+
json: args.includes('--json'),
|
|
36
|
+
listExamples: args.includes('--list-examples'),
|
|
37
|
+
tag: (() => {
|
|
38
|
+
const i = args.indexOf('--tag');
|
|
39
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : null;
|
|
40
|
+
})(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const log = (msg) => {
|
|
44
|
+
if (!flags.json) {
|
|
45
|
+
console.log(msg);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
async function runIn(dir, cmd) {
|
|
50
|
+
return execAsync(cmd, { cwd: dir, maxBuffer: 32 * 1024 * 1024 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runInTarget(cmd) {
|
|
54
|
+
return runIn(TARGET_DIR, cmd);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getLatestNpmVersion() {
|
|
58
|
+
try {
|
|
59
|
+
const { stdout } = await execAsync(`npm view ${PACKAGE_NAME} version`, {
|
|
60
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
61
|
+
});
|
|
62
|
+
return stdout.trim() || null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function isGitRepo(dir) {
|
|
69
|
+
if (!existsSync(dir)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await runIn(dir, 'git rev-parse --is-inside-work-tree');
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getCurrentSha() {
|
|
81
|
+
const { stdout } = await runInTarget('git rev-parse --short HEAD');
|
|
82
|
+
return stdout.trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function getCurrentRef() {
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await runInTarget('git symbolic-ref --short HEAD');
|
|
88
|
+
return { type: 'branch', name: stdout.trim() };
|
|
89
|
+
} catch {
|
|
90
|
+
try {
|
|
91
|
+
const { stdout } = await runInTarget('git describe --tags --exact-match');
|
|
92
|
+
return { type: 'tag', name: stdout.trim() };
|
|
93
|
+
} catch {
|
|
94
|
+
const { stdout } = await runInTarget('git rev-parse --short HEAD');
|
|
95
|
+
return { type: 'detached', name: stdout.trim() };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getDefaultBranch() {
|
|
101
|
+
try {
|
|
102
|
+
const { stdout } = await runInTarget('git symbolic-ref --short refs/remotes/origin/HEAD');
|
|
103
|
+
return stdout.trim().replace(/^origin\//, '');
|
|
104
|
+
} catch {
|
|
105
|
+
try {
|
|
106
|
+
await runInTarget('git rev-parse --verify main');
|
|
107
|
+
return 'main';
|
|
108
|
+
} catch {
|
|
109
|
+
return 'master';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function cloneRepo() {
|
|
115
|
+
log(`Cloning ${REPO_URL} -> ${TARGET_DIR}`);
|
|
116
|
+
await execAsync(`git clone "${REPO_URL}" "${TARGET_DIR}"`, {
|
|
117
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function pullRepo() {
|
|
122
|
+
const branch = await getDefaultBranch();
|
|
123
|
+
log(`Updating ${TARGET_DIR} on ${branch}`);
|
|
124
|
+
try {
|
|
125
|
+
await runInTarget(`git checkout ${branch}`);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
log(`Note: could not checkout ${branch} (${e.message.trim()})`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await runInTarget(`git pull --ff-only origin ${branch}`);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
log(`Pull failed: ${e.message.trim()}`);
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await runInTarget('git fetch --tags --force');
|
|
137
|
+
} catch (e) {
|
|
138
|
+
log(`Tag fetch failed: ${e.message.trim()}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function checkoutTag(tag) {
|
|
143
|
+
log(`Checking out ${tag}`);
|
|
144
|
+
try {
|
|
145
|
+
await runInTarget('git fetch --tags --force');
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log(`Tag fetch warning: ${e.message.trim()}`);
|
|
148
|
+
}
|
|
149
|
+
await runInTarget(`git checkout ${tag}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readPackageDescription(dir) {
|
|
153
|
+
const pkgPath = join(dir, 'package.json');
|
|
154
|
+
if (!existsSync(pkgPath)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
159
|
+
return pkg.description || null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readReadmeSnippet(dir) {
|
|
166
|
+
for (const name of ['README.md', 'Readme.md', 'readme.md']) {
|
|
167
|
+
const p = join(dir, name);
|
|
168
|
+
if (!existsSync(p)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const raw = readFileSync(p, 'utf-8');
|
|
173
|
+
const lines = raw.split(/\r?\n/);
|
|
174
|
+
let heading = null;
|
|
175
|
+
const paraLines = [];
|
|
176
|
+
let inPara = false;
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
if (!heading) {
|
|
179
|
+
const m = line.match(/^#\s+(.+)$/);
|
|
180
|
+
if (m) {
|
|
181
|
+
heading = m[1].trim();
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (heading) {
|
|
186
|
+
if (!inPara) {
|
|
187
|
+
if (line.trim() === '') {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (line.startsWith('#')) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
inPara = true;
|
|
194
|
+
paraLines.push(line);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (line.trim() === '') {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (line.startsWith('#')) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
paraLines.push(line);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { heading, paragraph: paraLines.join(' ').trim() || null };
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function listExamples() {
|
|
215
|
+
const examplesDir = join(TARGET_DIR, 'examples');
|
|
216
|
+
if (!existsSync(examplesDir)) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return readdirSync(examplesDir)
|
|
221
|
+
.filter((name) => {
|
|
222
|
+
const full = join(examplesDir, name);
|
|
223
|
+
try {
|
|
224
|
+
return statSync(full).isDirectory();
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
.sort()
|
|
230
|
+
.map((name) => {
|
|
231
|
+
const dir = join(examplesDir, name);
|
|
232
|
+
const description = readPackageDescription(dir);
|
|
233
|
+
const readme = readReadmeSnippet(dir);
|
|
234
|
+
return {
|
|
235
|
+
name,
|
|
236
|
+
relativePath: `examples/${name}`,
|
|
237
|
+
description,
|
|
238
|
+
readmeHeading: readme?.heading || null,
|
|
239
|
+
readmeOpening: readme?.paragraph || null,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function main() {
|
|
245
|
+
let action;
|
|
246
|
+
|
|
247
|
+
if (await isGitRepo(TARGET_DIR)) {
|
|
248
|
+
action = 'updated';
|
|
249
|
+
await pullRepo();
|
|
250
|
+
} else if (existsSync(TARGET_DIR)) {
|
|
251
|
+
const msg = `Path exists but is not a git repository: ${TARGET_DIR}`;
|
|
252
|
+
if (flags.json) {
|
|
253
|
+
process.stdout.write(JSON.stringify({ ok: false, error: msg }) + '\n');
|
|
254
|
+
} else {
|
|
255
|
+
console.error(msg);
|
|
256
|
+
console.error('Move or remove the folder and rerun.');
|
|
257
|
+
}
|
|
258
|
+
process.exit(1);
|
|
259
|
+
} else {
|
|
260
|
+
action = 'cloned';
|
|
261
|
+
await cloneRepo();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const latestVersion = await getLatestNpmVersion();
|
|
265
|
+
|
|
266
|
+
let tagToCheckout = flags.tag;
|
|
267
|
+
if (tagToCheckout === 'latest') {
|
|
268
|
+
if (!latestVersion) {
|
|
269
|
+
log('Warning: could not resolve latest npm version, staying on default branch.');
|
|
270
|
+
tagToCheckout = null;
|
|
271
|
+
} else {
|
|
272
|
+
tagToCheckout = `v${latestVersion}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (tagToCheckout) {
|
|
277
|
+
await checkoutTag(tagToCheckout);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const sha = await getCurrentSha();
|
|
281
|
+
const ref = await getCurrentRef();
|
|
282
|
+
|
|
283
|
+
const result = {
|
|
284
|
+
ok: true,
|
|
285
|
+
action,
|
|
286
|
+
path: TARGET_DIR,
|
|
287
|
+
ref: ref.name,
|
|
288
|
+
refType: ref.type,
|
|
289
|
+
commit: sha,
|
|
290
|
+
latestNpmVersion: latestVersion,
|
|
291
|
+
package: PACKAGE_NAME,
|
|
292
|
+
repoUrl: REPO_URL,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (flags.listExamples) {
|
|
296
|
+
result.examples = listExamples();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (flags.json) {
|
|
300
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
log('');
|
|
305
|
+
log(`Action: ${result.action}`);
|
|
306
|
+
log(`Path: ${result.path}`);
|
|
307
|
+
log(`Ref: ${ref.type} ${ref.name}`);
|
|
308
|
+
log(`Commit: ${sha}`);
|
|
309
|
+
log(`Latest on npm: ${latestVersion ?? '(unavailable)'}`);
|
|
310
|
+
if (flags.listExamples && result.examples) {
|
|
311
|
+
log('');
|
|
312
|
+
log(`Examples (${result.examples.length}):`);
|
|
313
|
+
for (const ex of result.examples) {
|
|
314
|
+
log(` - ${ex.name}${ex.description ? ` -- ${ex.description}` : ''}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
main().catch((err) => {
|
|
320
|
+
const msg = err && err.message ? err.message : String(err);
|
|
321
|
+
if (flags.json) {
|
|
322
|
+
process.stdout.write(JSON.stringify({ ok: false, error: msg }) + '\n');
|
|
323
|
+
} else {
|
|
324
|
+
console.error(`Error: ${msg}`);
|
|
325
|
+
}
|
|
326
|
+
process.exit(1);
|
|
327
|
+
});
|