mcplane 0.4.0 → 0.5.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +108 -18
  3. package/dist/agent-plugins.d.ts +6 -0
  4. package/dist/agent-plugins.js +76 -0
  5. package/dist/cdp.d.ts +49 -0
  6. package/dist/cdp.js +253 -0
  7. package/dist/checks/ard.d.ts +67 -0
  8. package/dist/checks/ard.js +215 -0
  9. package/dist/checks/auth.js +2 -1
  10. package/dist/checks/chain.d.ts +45 -0
  11. package/dist/checks/chain.js +171 -0
  12. package/dist/checks/listing.js +23 -14
  13. package/dist/checks/oauth.d.ts +45 -0
  14. package/dist/checks/oauth.js +64 -0
  15. package/dist/checks/readiness.d.ts +58 -0
  16. package/dist/checks/readiness.js +230 -0
  17. package/dist/checks/tools.js +38 -7
  18. package/dist/checks/vercel.d.ts +27 -0
  19. package/dist/checks/vercel.js +153 -0
  20. package/dist/cli.js +54 -6
  21. package/dist/core.d.ts +1 -0
  22. package/dist/core.js +20 -4
  23. package/dist/demo.d.ts +60 -0
  24. package/dist/demo.js +573 -0
  25. package/dist/drift.js +10 -3
  26. package/dist/evals.d.ts +34 -0
  27. package/dist/evals.js +75 -0
  28. package/dist/extras.js +1 -1
  29. package/dist/images.d.ts +21 -0
  30. package/dist/images.js +99 -0
  31. package/dist/listings.d.ts +14 -0
  32. package/dist/listings.js +40 -1
  33. package/dist/mcp-client.d.ts +1 -5
  34. package/dist/mcp-client.js +8 -1
  35. package/dist/mcp-server.js +21 -11
  36. package/dist/packs.d.ts +48 -18
  37. package/dist/packs.js +380 -90
  38. package/dist/publish.js +13 -4
  39. package/dist/report.js +30 -13
  40. package/dist/skills.d.ts +21 -0
  41. package/dist/skills.js +180 -0
  42. package/dist/submissions.js +2 -1
  43. package/dist/types.d.ts +37 -4
  44. package/dist/types.js +1 -0
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/dist/video.d.ts +58 -0
  48. package/dist/video.js +190 -0
  49. package/dist/zip.d.ts +16 -2
  50. package/dist/zip.js +65 -5
  51. package/package.json +4 -1
  52. package/schema.json +86 -12
  53. package/schemas/agent-plugins-1.0.0/LICENSE +202 -0
  54. package/schemas/agent-plugins-1.0.0/mcp.schema.json +120 -0
  55. package/schemas/agent-plugins-1.0.0/plugin.schema.json +65 -0
package/dist/publish.js CHANGED
@@ -233,15 +233,24 @@ function prepared(m, store) {
233
233
  return { store, done: false, lines: [`smithery mcp publish ${m.server.url} -n @<your-org>/${m.name}`, 'Or at https://smithery.ai/new. If Smithery’s scan can’t get past your sign-in, serve /.well-known/mcp/server-card.json describing your tools.'] };
234
234
  }
235
235
  if (store === 'glama') {
236
+ const reg = registryName(m);
236
237
  const owner = gh?.owner ?? '<your-github-user>';
238
+ const domainNs = !reg.startsWith('io.github.');
239
+ const domain = reg.split('/')[0].split('.').reverse().join('.');
237
240
  return {
238
241
  store,
239
242
  done: false,
240
243
  lines: [
241
- `Hosted servers: Glama imports a connector from the official registry at https://glama.ai/mcp/connectors/${registryName(m)}. Publish there first, then claim it.`,
242
- `Code on GitHub: add ${gh ? `https://github.com/${gh.owner}/${gh.repo}` : 'the repo'} at https://glama.ai/mcp/servers ("Add MCP Server"); Glama checks license, security and health, and scores it.`,
243
- 'To claim either, commit glama.json at the repo root:',
244
- JSON.stringify({ $schema: 'https://glama.ai/mcp/schemas/server.json', maintainers: [owner] }, null, 2),
244
+ `Hosted server: Glama imports it from the official registry as a connector, https://glama.ai/mcp/connectors/${reg}. Publish there first.`,
245
+ domainNs
246
+ ? `To claim it: open that page, choose Claim ownership, sign in, and verify ${domain} with the token Glama shows, either as a TXT record at _glama-claim.${domain} or as https://${domain}/.well-known/glama.json ({"$schema":"https://glama.ai/mcp/schemas/connector.json","claim":"glama_claim_…"}). One verification covers every connector under the domain.`
247
+ : `To claim it: open that page, choose Claim ownership and "Claim with GitHub" as ${reg.split('/')[0].slice('io.github.'.length)}. An organisation also needs the Glama GitHub App installed.`,
248
+ ...(gh
249
+ ? [
250
+ `Server code on GitHub is a separate listing: add https://github.com/${gh.owner}/${gh.repo} at https://glama.ai/mcp/servers ("Add MCP Server") and claim it with glama.json at the repo root:`,
251
+ JSON.stringify({ $schema: 'https://glama.ai/mcp/schemas/server.json', maintainers: [owner] }, null, 2),
252
+ ]
253
+ : []),
245
254
  'Both awesome lists require the Glama badge on each entry, so do this first.',
246
255
  ],
247
256
  };
package/dist/report.js CHANGED
@@ -1,25 +1,41 @@
1
1
  import { STORE_NAMES } from './types.js';
2
2
  const tty = process.stdout.isTTY;
3
3
  const c = (code, s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
4
- const MARK = { pass: c('32', '✓'), warn: c('33', '!'), fail: c('31', '✗'), skip: c('90', '-') };
5
- export function printPreflight(r, opts = {}) {
6
- const fails = r.checks.filter((x) => x.level === 'fail');
7
- const warns = r.checks.filter((x) => x.level === 'warn');
8
- console.log(c('1', `Preflight for ${r.stores.map((s) => STORE_NAMES[s]).join(', ')}`));
9
- console.log('');
10
- for (const x of r.checks) {
11
- if (x.level === 'pass' && !opts.verbose)
4
+ const MARK = { pass: c('32', '✓'), warn: c('33', '!'), fail: c('31', '✗'), skip: c('90', '-'), info: c('36', 'i') };
5
+ /** Readiness for the next MCP revision gets its own section, so it never reads as a store requirement. */
6
+ const isReadiness = (x) => x.id.startsWith('readiness.');
7
+ function printChecks(checks, verbose, indent = '') {
8
+ for (const x of checks) {
9
+ if (x.level === 'pass' && !verbose)
12
10
  continue;
13
11
  const where = x.stores?.length ? c('90', ` [${x.stores.map((s) => STORE_NAMES[s]).join(', ')}]`) : '';
14
- console.log(`${MARK[x.level]} ${x.title}${where}`);
12
+ console.log(`${indent}${MARK[x.level]} ${x.title}${where}`);
15
13
  if (x.detail)
16
- console.log(c('90', ` ${x.detail}`));
14
+ console.log(c('90', `${indent} ${x.detail}`));
17
15
  if (x.fix && x.level !== 'pass')
18
- console.log(` ${x.fix}`);
16
+ console.log(`${indent} ${x.fix}`);
19
17
  }
20
- const passed = r.checks.length - fails.length - warns.length;
18
+ }
19
+ export function printPreflight(r, opts = {}) {
20
+ const fails = r.checks.filter((x) => x.level === 'fail');
21
+ const warns = r.checks.filter((x) => x.level === 'warn');
22
+ const notes = r.checks.filter((x) => x.level === 'info');
23
+ const main = r.checks.filter((x) => !isReadiness(x));
24
+ const ready = r.checks.filter(isReadiness);
25
+ console.log(c('1', `Preflight for ${r.stores.map((s) => STORE_NAMES[s]).join(', ')}`));
26
+ console.log('');
27
+ printChecks(main, opts.verbose);
28
+ const passed = main.filter((x) => x.level === 'pass').length;
21
29
  if (!opts.verbose && passed)
22
30
  console.log(c('32', `✓ ${passed} checks passed`) + c('90', ' (--verbose to list them)'));
31
+ if (ready.length) {
32
+ console.log('');
33
+ console.log(c('1', 'Readiness for MCP 2026-07-28') + c('90', ' (never blocking)'));
34
+ printChecks(ready, opts.verbose, ' ');
35
+ const ok = ready.filter((x) => x.level === 'pass').length;
36
+ if (!opts.verbose && ok)
37
+ console.log(` ${c('32', `✓ ${ok} ready`)}`);
38
+ }
23
39
  if (r.reminders.length) {
24
40
  console.log('');
25
41
  console.log(c('1', 'Check these yourself'));
@@ -27,5 +43,6 @@ export function printPreflight(r, opts = {}) {
27
43
  console.log(` • ${t}`);
28
44
  }
29
45
  console.log('');
30
- console.log(fails.length ? c('31', `${fails.length} blocking, ${warns.length} to review.`) : warns.length ? c('33', `No blockers, ${warns.length} to review.`) : c('32', 'Ready to submit.'));
46
+ const noteLine = notes.length ? `, ${notes.length} note${notes.length === 1 ? '' : 's'}` : '';
47
+ console.log(fails.length ? c('31', `${fails.length} blocking, ${warns.length} to review${noteLine}.`) : warns.length ? c('33', `No blockers, ${warns.length} to review${noteLine}.`) : c('32', `Ready to submit${noteLine ? ` (${notes.length} note${notes.length === 1 ? '' : 's'})` : ''}.`));
31
48
  }
@@ -0,0 +1,21 @@
1
+ import type { ZipEntry } from './zip.js';
2
+ export type FrontMatter = {
3
+ data: Record<string, unknown>;
4
+ body: string;
5
+ } | {
6
+ error: string;
7
+ };
8
+ /**
9
+ * Reads SKILL.md front matter: the YAML a skill loader sees, for the subset skills use (plain,
10
+ * quoted and block scalars, nested maps and lists). A plain value holding ": " is an error, as in
11
+ * YAML itself, because strict loaders refuse the whole skill over it.
12
+ */
13
+ export declare function frontMatter(raw: string): FrontMatter;
14
+ export interface SkillBundle {
15
+ files: ZipEntry[];
16
+ names: string[];
17
+ problems: string[];
18
+ todo: string[];
19
+ }
20
+ /** Reads, checks and lays out skills for skills/<folder>/ in a plugin package. */
21
+ export declare function bundleSkills(spec: string | string[] | undefined, dir: string, pluginName: string): Promise<SkillBundle>;
package/dist/skills.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Agent Skills for plugin packages: each skill is a folder with a SKILL.md, bundled under
3
+ * skills/<folder>/ with its scripts and references, executable bits kept. Checked against the
4
+ * Agent Skills spec (agentskills.io) and the skill rules in OpenAI's submission errors.
5
+ */
6
+ import { lstat, readFile, readdir } from 'node:fs/promises';
7
+ import { basename, join, relative, resolve, sep } from 'node:path';
8
+ /** Front matter fields the Agent Skills spec defines. */
9
+ const SPEC_FIELDS = new Set(['name', 'description', 'license', 'compatibility', 'metadata', 'allowed-tools']);
10
+ const NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
11
+ const DESCRIPTION_MAX = 1024;
12
+ /** Noise and bulk that never belongs in a package. */
13
+ const SKIPPED = new Set(['.DS_Store', 'Thumbs.db', 'node_modules', '.git']);
14
+ function unquoteDouble(s) {
15
+ try {
16
+ return JSON.parse(s.replace(/\\'/g, "'").replace(/\\\n/g, ''));
17
+ }
18
+ catch {
19
+ return s.slice(1, -1);
20
+ }
21
+ }
22
+ /**
23
+ * Reads SKILL.md front matter: the YAML a skill loader sees, for the subset skills use (plain,
24
+ * quoted and block scalars, nested maps and lists). A plain value holding ": " is an error, as in
25
+ * YAML itself, because strict loaders refuse the whole skill over it.
26
+ */
27
+ export function frontMatter(raw) {
28
+ const lines = raw.replace(/^/, '').replace(/\r\n/g, '\n').split('\n');
29
+ if (lines[0].trimEnd() !== '---')
30
+ return { error: 'SKILL.md must start with front matter between --- lines' };
31
+ const close = lines.findIndex((l, i) => i > 0 && /^(---|\.\.\.)\s*$/.test(l));
32
+ if (close < 0)
33
+ return { error: 'the front matter has no closing --- line' };
34
+ const fm = lines.slice(1, close);
35
+ const data = {};
36
+ for (let i = 0; i < fm.length; i++) {
37
+ const line = fm[i];
38
+ if (!line.trim() || /^\s*#/.test(line))
39
+ continue;
40
+ const m = /^([A-Za-z0-9_-]+)\s*:(?:[ \t]+(.*))?$/.exec(line);
41
+ if (!m)
42
+ return { error: `front matter line ${i + 2} isn't "key: value": ${line.trim().slice(0, 60)}` };
43
+ const key = m[1];
44
+ const value = (m[2] ?? '').trim();
45
+ const more = [];
46
+ while (i + 1 < fm.length && (fm[i + 1].trim() === '' || /^\s/.test(fm[i + 1])))
47
+ more.push(fm[++i]);
48
+ while (more.length && !more[more.length - 1].trim())
49
+ more.pop();
50
+ if (/^[|>][+-]?\d*(\s+#.*)?$/.test(value)) {
51
+ const indent = Math.min(...more.filter((l) => l.trim()).map((l) => l.length - l.trimStart().length));
52
+ const body = more.map((l) => l.slice(Number.isFinite(indent) ? indent : 0));
53
+ data[key] = value.startsWith('|') ? body.join('\n') : body.reduce((s, l) => (!l.trim() ? `${s}\n` : s && !s.endsWith('\n') ? `${s} ${l}` : `${s}${l}`), '');
54
+ }
55
+ else if (!value && more.length) {
56
+ const items = more.filter((l) => l.trim());
57
+ data[key] = items.every((l) => /^\s*-\s/.test(l))
58
+ ? items.map((l) => l.replace(/^\s*-\s+/, '').trim())
59
+ : Object.fromEntries(items.map((l) => /^\s*([^:]+):\s*(.*)$/.exec(l)).filter(Boolean).map((r) => [r[1].trim(), r[2].trim().replace(/^(["'])(.*)\1$/, '$2')]));
60
+ }
61
+ else if (value.startsWith('"')) {
62
+ data[key] = unquoteDouble([value, ...more.map((l) => l.trim())].join(' '));
63
+ }
64
+ else if (value.startsWith("'")) {
65
+ data[key] = [value, ...more.map((l) => l.trim())].join(' ').slice(1, -1).replace(/''/g, "'");
66
+ }
67
+ else {
68
+ const text = [value, ...more.map((l) => l.trim())].join(' ').replace(/\s+#.*$/, '');
69
+ if (/:\s/.test(text))
70
+ return { error: `"${key}" holds ": " without quotes, which YAML reads as a nested key; quote the value` };
71
+ data[key] = text;
72
+ }
73
+ }
74
+ return { data, body: lines.slice(close + 1).join('\n') };
75
+ }
76
+ async function walk(root, problems, skipped, at = root) {
77
+ const out = [];
78
+ for (const name of (await readdir(at)).sort()) {
79
+ const abs = join(at, name);
80
+ const inner = relative(root, abs).split(sep).join('/');
81
+ if (SKIPPED.has(name))
82
+ continue;
83
+ if (name.startsWith('.')) {
84
+ skipped.push(inner);
85
+ continue;
86
+ }
87
+ const st = await lstat(abs);
88
+ if (st.isSymbolicLink())
89
+ problems.push(`${basename(root)}/${inner} is a symbolic link; packages can't hold one (OpenAI ignores the skill). Copy the file in instead`);
90
+ else if (st.isDirectory())
91
+ out.push(...(await walk(root, problems, skipped, abs)));
92
+ else if (st.isFile())
93
+ out.push({ inner, abs, mode: st.mode & 0o111 ? 0o755 : 0o644 });
94
+ }
95
+ return out;
96
+ }
97
+ /** Skill folders from the manifest setting: one folder of skills, or a list of skill folders. Null when the folder isn't there. */
98
+ async function skillDirs(spec, dir) {
99
+ if (Array.isArray(spec))
100
+ return spec.map((p) => resolve(dir, p));
101
+ const parent = resolve(dir, spec);
102
+ const names = await readdir(parent).catch(() => null);
103
+ if (!names)
104
+ return null;
105
+ const dirs = [];
106
+ for (const n of names.sort())
107
+ if (!n.startsWith('.') && !SKIPPED.has(n) && (await lstat(join(parent, n))).isDirectory())
108
+ dirs.push(join(parent, n));
109
+ return dirs;
110
+ }
111
+ /** Reads, checks and lays out skills for skills/<folder>/ in a plugin package. */
112
+ export async function bundleSkills(spec, dir, pluginName) {
113
+ const out = { files: [], names: [], problems: [], todo: [] };
114
+ if (spec === undefined)
115
+ return out;
116
+ const dirs = await skillDirs(spec, dir);
117
+ if (!dirs) {
118
+ out.problems.push(`chatgpt.skills: ${spec} isn't a folder (paths are relative to mcplane.json)`);
119
+ return out;
120
+ }
121
+ if (!dirs.length && typeof spec === 'string')
122
+ out.todo.push(`chatgpt.skills: ${spec} holds no skill folders, so the package has no skills.`);
123
+ const seen = new Map();
124
+ for (const root of dirs) {
125
+ const folder = basename(root);
126
+ const where = `skills/${folder}`;
127
+ const st = await lstat(root).catch(() => null);
128
+ if (!st?.isDirectory()) {
129
+ out.problems.push(`${where}: ${relative(dir, root) || root} isn't a folder`);
130
+ continue;
131
+ }
132
+ const skipped = [];
133
+ const found = await walk(root, out.problems, skipped);
134
+ if (skipped.length)
135
+ out.todo.push(`Left out of ${where}: ${skipped.join(', ')} (hidden files never go in the package).`);
136
+ const md = found.find((f) => f.inner === 'SKILL.md');
137
+ if (!md) {
138
+ out.problems.push(`${where} has no SKILL.md`);
139
+ continue;
140
+ }
141
+ const raw = await readFile(md.abs, 'utf8');
142
+ const fm = frontMatter(raw);
143
+ if ('error' in fm) {
144
+ out.problems.push(`${where}/SKILL.md: ${fm.error}`);
145
+ continue;
146
+ }
147
+ const { name, description } = fm.data;
148
+ if (typeof name !== 'string' || !name.trim())
149
+ out.problems.push(`${where}/SKILL.md: "name" is required`);
150
+ else {
151
+ if (!NAME.test(name) || name.length > 64)
152
+ out.problems.push(`${where}/SKILL.md: name "${name}" must be lowercase letters, digits and single hyphens, at most 64 characters`);
153
+ if (name !== folder)
154
+ out.problems.push(`${where}/SKILL.md: name "${name}" doesn't match its folder "${folder}"; the Agent Skills spec requires a match, and strict clients skip the skill`);
155
+ if (`${pluginName}:${name}`.length > 64)
156
+ out.problems.push(`${where}: "${pluginName}:${name}" is over 64 characters, OpenAI's limit for a skill's full name`);
157
+ if (seen.has(name))
158
+ out.problems.push(`${where}: the name "${name}" is also used by ${seen.get(name)}`);
159
+ seen.set(name, where);
160
+ out.names.push(name);
161
+ }
162
+ if (typeof description !== 'string' || !description.trim())
163
+ out.problems.push(`${where}/SKILL.md: "description" is required`);
164
+ else if (description.trim().length > DESCRIPTION_MAX)
165
+ out.problems.push(`${where}/SKILL.md: the description is ${description.trim().length} characters; the limit is ${DESCRIPTION_MAX}`);
166
+ if (!fm.body.trim())
167
+ out.problems.push(`${where}/SKILL.md has no instructions after the front matter`);
168
+ const extra = Object.keys(fm.data).filter((k) => !SPEC_FIELDS.has(k));
169
+ if (extra.length)
170
+ out.todo.push(`${where}/SKILL.md: ${extra.map((k) => `"${k}"`).join(', ')} ${extra.length === 1 ? "isn't" : "aren't"} in the Agent Skills spec. OpenAI ignores unknown fields, but strict clients may refuse the skill.`);
171
+ const own = new Set(found.map((f) => f.inner));
172
+ for (const ref of new Set([...raw.matchAll(/\bscripts\/[A-Za-z0-9._/-]*[A-Za-z0-9_-]/g)].map((r) => r[0]))) {
173
+ if (!own.has(ref))
174
+ out.problems.push(`${where}/SKILL.md refers to ${ref}, which isn't in the skill's folder. Installed on its own, the skill can't run it`);
175
+ }
176
+ for (const f of found)
177
+ out.files.push({ path: `${where}/${f.inner}`, data: await readFile(f.abs), mode: f.mode });
178
+ }
179
+ return out;
180
+ }
@@ -17,6 +17,7 @@ const RT_SLUG = {
17
17
  'claude-plugins': 'claude-plugins',
18
18
  cursor: 'cursor',
19
19
  muse: 'muse',
20
+ 'vercel-connect': 'vercel-connect',
20
21
  };
21
22
  const TRACKED_FROM_GITHUB = ['grok', 'docker'];
22
23
  export async function load(dir = process.cwd()) {
@@ -36,7 +37,7 @@ const today = () => new Date().toISOString().slice(0, 10);
36
37
  function listingRef(m, store, appId) {
37
38
  if (store === 'chatgpt')
38
39
  return appId;
39
- if (store === 'claude-connectors' || store === 'muse')
40
+ if (store === 'claude-connectors' || store === 'muse' || store === 'vercel-connect')
40
41
  return m.server.url;
41
42
  if (store === 'claude-plugins' || store === 'cursor')
42
43
  return m.repository;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export type Level = 'pass' | 'warn' | 'fail' | 'skip';
1
+ /** info: optional or advisory. It never blocks and isn't counted as something to review. */
2
+ export type Level = 'pass' | 'warn' | 'fail' | 'skip' | 'info';
2
3
  export interface Check {
3
4
  id: string;
4
5
  level: Level;
@@ -10,7 +11,7 @@ export interface Check {
10
11
  /** The stores this matters for; empty means all. */
11
12
  stores?: StoreId[];
12
13
  }
13
- export type StoreId = 'mcp-registry' | 'chatgpt' | 'claude-connectors' | 'claude-plugins' | 'cursor' | 'grok' | 'docker' | 'muse' | 'smithery' | 'glama' | 'cline' | 'lobehub' | 'awesome-mcp-servers' | 'awesome-remote-mcp-servers';
14
+ export type StoreId = 'mcp-registry' | 'chatgpt' | 'claude-connectors' | 'claude-plugins' | 'cursor' | 'grok' | 'docker' | 'muse' | 'smithery' | 'glama' | 'cline' | 'lobehub' | 'awesome-mcp-servers' | 'awesome-remote-mcp-servers' | 'vercel-connect';
14
15
  export declare const STORE_NAMES: Record<StoreId, string>;
15
16
  export interface Manifest {
16
17
  $schema?: string;
@@ -23,6 +24,11 @@ export interface Manifest {
23
24
  server: {
24
25
  url: string;
25
26
  auth?: 'none' | 'oauth';
27
+ /**
28
+ * Sign-in pages and identity-provider URLs your consent screen sends people to (e.g. https://accounts.example.com/sign-in).
29
+ * Preflight GETs them with the authorization endpoint to find bot challenges and 403s reviewers would hit.
30
+ */
31
+ signIn?: string[];
26
32
  };
27
33
  repository?: string;
28
34
  author?: {
@@ -36,6 +42,7 @@ export interface Manifest {
36
42
  terms?: string;
37
43
  docs?: string;
38
44
  };
45
+ /** A direct image URL. The ChatGPT package also takes a local path, relative to mcplane.json. */
39
46
  icon?: string;
40
47
  prompts?: string[];
41
48
  stores?: StoreId[];
@@ -54,7 +61,10 @@ export interface Manifest {
54
61
  prompt: string;
55
62
  }[];
56
63
  };
57
- /** Hand-written ChatGPT hint justifications, per tool; they replace mcplane's drafts. */
64
+ /**
65
+ * @deprecated OpenAI no longer asks for hint justifications. Still accepted: the ChatGPT pack lists them
66
+ * as optional notes for an appeal if review flags a hint.
67
+ */
58
68
  justifications?: Record<string, {
59
69
  readOnly?: string;
60
70
  openWorld?: string;
@@ -77,9 +87,15 @@ export interface Manifest {
77
87
  /** #RRGGBB with 2:1 contrast against white; the dark one against #212121. */
78
88
  brandColor?: string;
79
89
  brandColorDark?: string;
80
- /** Direct image URLs; default to "icon". */
90
+ /** Image URLs or local paths (relative to mcplane.json), bundled into the package. logo and composerIcon default to "icon". */
91
+ logo?: string;
81
92
  composerIcon?: string;
82
93
  logoDark?: string;
94
+ /**
95
+ * Agent Skills to bundle under skills/ in the package: a folder whose subfolders each hold a SKILL.md,
96
+ * or a list of skill folders. Paths are relative to mcplane.json.
97
+ */
98
+ skills?: string | string[];
83
99
  /** Reviewer-accessible video URL showing the main use cases (required for review). */
84
100
  demoVideo?: string;
85
101
  commerce?: boolean;
@@ -100,6 +116,23 @@ export interface Manifest {
100
116
  keywords?: string[];
101
117
  domains?: string[];
102
118
  };
119
+ /** Vercel Connect's Submit a Service form fields with no shared equivalent. */
120
+ vercelConnect?: {
121
+ /** Base URL of a REST API your users also call, listed as a second target. */
122
+ apiBase?: string;
123
+ /** Default scopes for the OAuth method. Defaults to the discovery document's scopes_supported. */
124
+ scopes?: string[];
125
+ /** Offer an API key connection method: where users create a key, and the header it's sent in. */
126
+ apiKey?: {
127
+ docs?: string;
128
+ header?: string;
129
+ };
130
+ };
131
+ /** The Claude Code plugin that ships this server, for "mcplane pack claude-eval". Read from the plugin repo when left out. */
132
+ claudePlugin?: {
133
+ name?: string;
134
+ server?: string;
135
+ };
103
136
  /** Named workflows, run with "mcplane <lane>": each step is a command line without "mcplane". */
104
137
  lanes?: Record<string, string[]>;
105
138
  /** How reviewers get in, for servers behind sign-in. Never put the password here; say where it lives. */
package/dist/types.js CHANGED
@@ -13,4 +13,5 @@ export const STORE_NAMES = {
13
13
  lobehub: 'LobeHub Market',
14
14
  'awesome-mcp-servers': 'awesome-mcp-servers',
15
15
  'awesome-remote-mcp-servers': 'awesome-remote-mcp-servers',
16
+ 'vercel-connect': 'Vercel Connect',
16
17
  };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.4.0";
1
+ export declare const VERSION = "0.5.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.4.0';
1
+ export const VERSION = '0.5.0';
@@ -0,0 +1,58 @@
1
+ /** One screencast frame: a JPEG relative to the work folder, and when it was shown (seconds). */
2
+ export interface Frame {
3
+ file: string;
4
+ ts: number;
5
+ }
6
+ export interface Scene {
7
+ caption: string;
8
+ frames: Frame[];
9
+ /** When recording stopped; the last frame is held until then. */
10
+ end: number;
11
+ }
12
+ /** ffmpeg's concat-demuxer list: each frame held until the next one, the last until `end`. */
13
+ export declare function concatList(frames: Frame[], end: number): string;
14
+ /** How long a scene runs once assembled. */
15
+ export declare function sceneSeconds(s: Scene): number;
16
+ /**
17
+ * Escapes a value for a filter option inside an ffmpeg filtergraph. Two levels:
18
+ * the option value (\ ' :) and then the filtergraph itself (\ ' [ ] , ;).
19
+ */
20
+ export declare function ffEscape(value: string): string;
21
+ /**
22
+ * A caption as at most `maxLines` lines of about `width` characters. Control
23
+ * characters become spaces (drawtext would draw them as boxes); anything that
24
+ * doesn't fit ends in an ellipsis.
25
+ */
26
+ export declare function wrapCaption(text: string, width?: number, maxLines?: number): string[];
27
+ /** Width and height from a JPEG's start-of-frame marker. */
28
+ export declare function jpegSize(b: Uint8Array): {
29
+ width: number;
30
+ height: number;
31
+ } | null;
32
+ /** Even dimensions (H.264 needs them) within the maximum, keeping the aspect ratio. */
33
+ export declare function outputSize(width: number, height: number, maxWidth?: number, maxHeight?: number): {
34
+ width: number;
35
+ height: number;
36
+ };
37
+ export declare function findFfmpeg(path?: string): string | null;
38
+ /** Text colours that read on a background, given as 0xRRGGBB: light on dark, dark on light. */
39
+ export declare function inkFor(bg: string): {
40
+ text: string;
41
+ muted: string;
42
+ };
43
+ /**
44
+ * Builds the video in `work` (where the frames are) and writes it to `out`.
45
+ * Every part is encoded the same way so they join without re-encoding.
46
+ */
47
+ export declare function buildVideo(o: {
48
+ scenes: Scene[];
49
+ title: string;
50
+ subtitle?: string;
51
+ out: string;
52
+ work: string;
53
+ ffmpeg: string;
54
+ size: {
55
+ width: number;
56
+ height: number;
57
+ };
58
+ }): Promise<number>;