@tanqory/dev-mcp 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -75,7 +75,9 @@ next.
75
75
  | `screenshot {path, viewport, locale?, market?, fullPage?}` | Returns a PNG, as an MCP image block, from local Playwright. |
76
76
  | `list_dev_stores` | Runs `tanqory stores --json` and reports the store this theme is linked to. |
77
77
  | `get_store_data_summary` | Calls `GET /__tq/store-summary` on the dev server: the products, collections, markets, languages and content you can design against. |
78
- | `push_draft`, `get_build_status`, `get_preview_url` | **Always refuse today** with `draft_push_unavailable`. The platform's build lane for developer themes is not live yet. These tools never upload, never install, never publish and never invent a URL. |
78
+ | `push_draft {confirm?, planDigest?}` | Two steps. Without `confirm`: runs `tanqory theme push --build-only --dry-run` and returns the plan (files, bytes, sha256, target development store, `planDigest`); nothing is uploaded. With `confirm: true` and that `planDigest`: runs `tanqory theme push --build-only --plan-digest <d>`: the platform builds the theme in its sandboxed lane, runs conformance and signs it, **private to your development store**. `plan_changed` if the theme changed in between. Nothing is installed or published. |
79
+ | `get_build_status {buildId?}` | Runs `tanqory theme builds [--id <buildId>] --json`: one build (status, verdict, conformance summary, signature, error) or the store's recent builds and the daily quota. |
80
+ | `get_preview_url` | **Refuses today** with `preview_unavailable`: a pushed build is not installed as a draft yet, so no preview URL exists. It never invents one. |
79
81
 
80
82
  **Prompts:** `new-theme`, `new-section`, `fix-check-report` and `port-from-another-template-language`.
81
83
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanqory/dev-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local MCP server (stdio) for building Tanqory storefront themes with coding agents: versioned docs and schemas, kit examples, section scaffolding, theme checks, and pages and screenshots of the running dev server.",
5
5
  "keywords": [
6
6
  "tanqory",
@@ -43,7 +43,7 @@
43
43
  "zod": "^3.25.76"
44
44
  },
45
45
  "devDependencies": {
46
- "create-tanqory-theme": "0.2.5"
46
+ "create-tanqory-theme": "0.2.6"
47
47
  },
48
48
  "publishConfig": {
49
49
  "registry": "https://npm.pkg.github.com"
package/src/server.mjs CHANGED
@@ -35,7 +35,7 @@ const INSTRUCTIONS = `Tanqory theme development (Theme ABI v2). Use these tools,
35
35
  4. Verify with check_theme, not \`npm run check\`: fix every failure it reports, including ones you did not cause, until passed is true.
36
36
  5. Stop the dev server with stop_dev_server (shell fallback \`npm run dev -- --stop\`). Never pkill, killall or a pid found by name.
37
37
  Never start \`tanqory mcp\`, Playwright or \`theme check\` yourself from a sandboxed shell: Chromium cannot start there; these tools do it.
38
- You are not done until check_theme returned passed: true and you have seen the change in a screenshot; never say a check is green otherwise. Every storefront function (cart, variants, search, filters, markets, forms, SEO, i18n, live edit) comes from @tanqory/theme-kit; the theme owns markup and CSS. This server never calls a model, never takes a store id and never publishes.`
38
+ You are not done until check_theme returned passed: true and you have seen the change in a screenshot; never say a check is green otherwise. Every storefront function (cart, variants, search, filters, markets, forms, SEO, i18n, live edit) comes from @tanqory/theme-kit; the theme owns markup and CSS. This server never calls a model, never takes a store id and never publishes; push_draft builds and signs privately for your development store and installs nothing.`
39
39
 
40
40
  const errorSchema = z.object({ code: z.string(), message: z.string(), hint: z.string().optional() }).passthrough()
41
41
  const base = { ok: z.boolean().describe('false when the tool could not do what was asked; see error'), error: errorSchema.optional() }
@@ -597,49 +597,97 @@ Two steps: call WITHOUT confirm to get the plan (every file, full new contents,
597
597
  }),
598
598
  )
599
599
 
600
- // ── draft push (backend not live yet: typed refusals) ─────────────────────
601
- const REFUSAL = {
602
- code: 'draft_push_unavailable',
600
+ // ── platform builds (P5): push_draft builds + signs privately; install/preview arrive with P6 ──
601
+ const PREVIEW_REFUSAL = {
602
+ code: 'preview_unavailable',
603
603
  message:
604
- "Pushing a theme draft is not available yet: the platform's sandboxed build lane for developer themes, draft install and preview channel are not live. Nothing was uploaded, built, installed or published, and no preview URL exists.",
605
- hint: 'Keep developing locally: `tanqory theme dev` + check_theme until every tier passes. When `tanqory theme push` ships, this tool will wrap it with the same two-step confirm.',
606
- instead: ['tanqory theme dev (localhost with store data)', 'check_theme (all tiers)', 'screenshot / get_page against the dev server'],
604
+ 'Installing a pushed theme as an unpublished draft, and its preview URL, are not available yet: push_draft builds, checks and signs the theme privately for your development store, and installs nothing. No preview URL exists, so none is returned.',
605
+ hint: 'Use localhost (dev_server_status / screenshot) to look at the theme. push_draft + get_build_status tell you whether the platform build passed.',
606
+ }
607
+ const buildOut = { ...base, mode: z.string().optional(), planDigest: z.string().optional(), buildId: z.string().optional(), verdict: z.string().optional(), build: z.any().optional(), store: z.any().optional(), theme: z.any().optional(), upload: z.any().optional(), notUploaded: z.any().optional(), builds: z.array(z.any()).optional(), quota: z.any().optional() }
608
+ const buildLines = (b) => {
609
+ const c = b?.conformance ?? {}
610
+ return [
611
+ `build ${b?.id}: ${b?.status} (verdict ${b?.verdict})${b?.signed ? ', signed' : ''}, private to store ${b?.storeId}`,
612
+ ...(c.status ? [`conformance: ${c.status}${c.basis ? ` on the ${c.basis}` : ''}`] : []),
613
+ ...(c.warnings ?? []).map((w) => `budget warning: ${w.name} ${w.value} ${w.unit} (budget ${w.budget})`),
614
+ ...(b?.error ? [`${b.status}: [${b.error.kind}${b.error.code ? `/${b.error.code}` : ''}] ${b.error.message}${b.error.path ? ` (${b.error.path})` : ''}`] : []),
615
+ ]
607
616
  }
608
- const refusalOut = { ...base, refused: z.literal(true).optional() }
609
- const refuse = () => ({ isError: true, content: [{ type: 'text', text: `${REFUSAL.code}: ${REFUSAL.message}\nWhat to do instead: ${REFUSAL.instead.join('; ')}. ${REFUSAL.hint}` }], structuredContent: { ok: false, refused: true, error: REFUSAL } })
610
617
 
611
618
  server.registerTool(
612
619
  'push_draft',
613
620
  {
614
- title: 'Push the theme as an unpublished draft (not available yet)',
615
- description: `Would upload the theme for a sandboxed platform build and install it as an UNPUBLISHED draft on your dev store (two-step: a plan, then confirm). ALWAYS REFUSES TODAY with ${REFUSAL.code}: the platform build lane is not live. It never uploads, never installs, never publishes, and never returns a preview URL. Do not report a push as done.`,
616
- inputSchema: { confirm: z.boolean().optional().describe('Second step (refused today)'), planDigest: z.string().optional() },
617
- outputSchema: refusalOut,
618
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
621
+ title: 'Build the theme on the platform (private, signed; installs nothing)',
622
+ description:
623
+ 'Uploads the theme SOURCE (git-tracked files, .gitignore respected; node_modules, dist, reports, .tanqory/dev and credential files are never uploaded) to the platform\'s sandboxed build lane for your linked DEVELOPMENT store, and waits for build → conformance → signing (`tanqory theme push --build-only`). Two steps: call WITHOUT confirm to get the plan (files, bytes, sha256, target store, planDigest; nothing is uploaded). Then call again with confirm: true and that planDigest; if the theme changed in between it refuses with plan_changed. The result is a signed build PRIVATE to that development store: nothing is installed, previewed or published, and there is no preview URL yet. Run check_theme first: a theme that fails it fails the platform build too. Needs a developer login (`tanqory login`) and a linked development store.',
624
+ inputSchema: {
625
+ confirm: z.boolean().optional().describe('true = upload the plan identified by planDigest'),
626
+ planDigest: z.string().max(100).optional().describe('From the plan step; required with confirm: true'),
627
+ },
628
+ outputSchema: buildOut,
629
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
619
630
  },
620
- guard(async () => refuse()),
631
+ guard(async ({ confirm, planDigest }) => {
632
+ const root = needTheme()
633
+ if (!confirm) {
634
+ const env = await runCli(['theme', 'push', '--build-only', '--dry-run'], { ...cliOpts, root })
635
+ if (!env.ok) return fail(env.error?.code ?? 'push_plan_failed', env.error?.message ?? '`tanqory theme push --dry-run` failed', env.error?.hint)
636
+ const d = env.data ?? {}
637
+ const text = [
638
+ `PLAN (nothing uploaded): ${d.upload?.files} files, ${d.upload?.bytes} bytes, sha256 ${String(d.upload?.sha256 ?? '').slice(0, 12)}…, theme "${d.theme?.handle}" → development store ${d.store?.handle ?? d.store?.id}.`,
639
+ ...(d.notUploaded?.secrets?.length ? [`Never uploaded (look like credentials): ${d.notUploaded.secrets.join(', ')}`] : []),
640
+ `To build it, call push_draft again with confirm: true, planDigest: "${d.planDigest}". The build is private to that development store; nothing is installed.`,
641
+ ].join('\n')
642
+ return done({ mode: 'plan', planDigest: d.planDigest, store: d.store, theme: d.theme, upload: d.upload, notUploaded: d.notUploaded }, text)
643
+ }
644
+ if (!planDigest) return fail('plan_digest_required', 'confirm: true needs the planDigest of a plan you reviewed.', 'Call push_draft without confirm first.')
645
+ const env = await runCli(['theme', 'push', '--build-only', '--plan-digest', planDigest], { ...cliOpts, root, timeoutMs: 25 * 60 * 1000 })
646
+ const d = env.data ?? {}
647
+ if (!env.ok) {
648
+ const lines = d.build ? buildLines(d.build) : []
649
+ return {
650
+ isError: true,
651
+ content: [{ type: 'text', text: [`${env.error?.code}: ${env.error?.message}`, ...lines, env.error?.hint ? `What to do: ${env.error.hint}` : ''].filter(Boolean).join('\n') }],
652
+ structuredContent: { ok: false, error: env.error ?? { code: 'push_failed', message: 'push failed' }, ...(d.build ? { mode: 'build', buildId: d.build.id, verdict: d.build.verdict, build: d.build, store: d.store, theme: d.theme } : {}), ...(d.planDigest ? { planDigest: d.planDigest } : {}) },
653
+ }
654
+ }
655
+ return done(
656
+ { mode: 'build', planDigest: d.planDigest, buildId: d.buildId, verdict: d.verdict, build: d.build, store: d.store, theme: d.theme, upload: d.upload, quota: d.quota },
657
+ [...buildLines(d.build), 'Nothing was installed or published; there is no preview URL yet. Follow up with get_build_status if it is still pending.'].join('\n'),
658
+ )
659
+ }),
621
660
  )
622
661
  server.registerTool(
623
662
  'get_build_status',
624
663
  {
625
- title: 'Status of a pushed draft build (not available yet)',
626
- description: `Would report the platform build of a pushed draft. ALWAYS REFUSES TODAY with ${REFUSAL.code}: no draft can be pushed yet, so no build exists.`,
627
- inputSchema: { buildId: z.string().max(100).optional() },
628
- outputSchema: refusalOut,
664
+ title: 'Status of your platform theme builds',
665
+ description:
666
+ 'With buildId: that build of your linked development store (status, verdict, conformance summary, signature, error with kind/code/path). Without: the store\'s recent builds and the daily build quota (`tanqory theme builds --json`). Read-only.',
667
+ inputSchema: { buildId: z.string().uuid().optional().describe('From push_draft; omit for the recent builds') },
668
+ outputSchema: buildOut,
629
669
  annotations: { ...READ, openWorldHint: true },
630
670
  },
631
- guard(async () => refuse()),
671
+ guard(async ({ buildId }) => {
672
+ const root = needTheme()
673
+ const env = await runCli(['theme', 'builds', ...(buildId ? ['--id', buildId] : [])], { ...cliOpts, root })
674
+ if (!env.ok) return fail(env.error?.code ?? 'builds_failed', env.error?.message ?? '`tanqory theme builds` failed', env.error?.hint)
675
+ const d = env.data ?? {}
676
+ if (buildId) return done({ buildId, verdict: d.build?.verdict, build: d.build, store: d.store }, buildLines(d.build).join('\n'))
677
+ const builds = d.builds ?? []
678
+ return done({ builds, store: d.store, quota: d.quota }, [`${builds.length} recent build(s) for ${d.store?.handle ?? d.store?.id}; ${d.quota?.used ?? '?'} of ${d.quota?.limit ?? '?'} used in 24 h`, ...builds.map((b) => `- ${b.id} ${b.status} (${b.verdict})${b.theme?.handle ? ` theme ${b.theme.handle}` : ''} ${b.createdAt}`)].join('\n'))
679
+ }),
632
680
  )
633
681
  server.registerTool(
634
682
  'get_preview_url',
635
683
  {
636
684
  title: 'Preview URL of a draft (not available yet)',
637
- description: `Would return the shareable preview and editor URLs of a pushed draft. ALWAYS REFUSES TODAY with ${REFUSAL.code}: there is no draft and no preview URL. Never invent one; localhost (dev_server_status) is the only preview today.`,
685
+ description: `Would return the shareable preview and editor URLs of an installed draft. ALWAYS REFUSES TODAY with ${PREVIEW_REFUSAL.code}: push_draft builds and signs privately but installs nothing, so no preview URL exists. Never invent one; localhost (dev_server_status) is the only preview today.`,
638
686
  inputSchema: {},
639
- outputSchema: refusalOut,
687
+ outputSchema: { ...base, refused: z.literal(true).optional() },
640
688
  annotations: { ...READ, openWorldHint: true },
641
689
  },
642
- guard(async () => refuse()),
690
+ guard(async () => ({ isError: true, content: [{ type: 'text', text: `${PREVIEW_REFUSAL.code}: ${PREVIEW_REFUSAL.message}\nWhat to do instead: ${PREVIEW_REFUSAL.hint}` }], structuredContent: { ok: false, refused: true, error: PREVIEW_REFUSAL } })),
643
691
  )
644
692
 
645
693
  // ── resources: every document as tanqory://docs/<id> ──────────────────────