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.
@@ -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`) for working examples and API documentation:
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
- git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git ./mcp-ext-apps
22
+ node scripts/clone-mcp-ext-apps.js --tag latest
20
23
  ```
21
24
 
22
- > Add `mcp-ext-apps/` to the project's `.gitignore` it's a reference clone, not part of the project.
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`) for working examples and API documentation:
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
- git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git ./mcp-ext-apps
51
+ node scripts/clone-mcp-ext-apps.js --tag latest
49
52
  ```
50
53
 
51
- > Add `mcp-ext-apps/` to the project's `.gitignore` it's a reference clone, not part of the project.
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
 
@@ -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
 
@@ -91,3 +91,4 @@ glm.sh
91
91
  /.playwright-mcp/
92
92
  /claudedocs/
93
93
  preferred-language.txt
94
+ /mcp-ext-apps/
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/sdk": "^1.29.0",
56
56
  "dotenv": "^17.4.1",
57
- "fa-mcp-sdk": "^0.4.100"
57
+ "fa-mcp-sdk": "^0.4.101"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/express": "^5.0.6",
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.100",
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
+ });