@myna-sh/mcp 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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ Configure your MCP client to run:
28
28
  }
29
29
  ```
30
30
 
31
- Use a narrowly scoped Myna API key and keep it out of source control.
31
+ Use a narrowly scoped Myna API key and keep it out of source control. A key can also be confined to specific collections (`myna keys create --collections changelog`), which is worth doing for an agent with one job: the server then cannot reach content outside it, whatever it is asked to do.
32
32
 
33
33
  ## Streamable HTTP
34
34
 
package/dist/main.js CHANGED
@@ -340,6 +340,12 @@ var ManagementClient = class {
340
340
  collections: (project, signal) => this.get(`/projects/${enc(project)}/collections`, void 0, signal),
341
341
  collection: (project, key, signal) => this.get(`/projects/${enc(project)}/collections/${enc(key)}`, void 0, signal),
342
342
  versions: (project, key, signal) => this.get(`/projects/${enc(project)}/collections/${enc(key)}/versions`, void 0, signal),
343
+ /** What is still untranslated in a collection, per project locale. */
344
+ translations: (project, key, signal) => this.get(
345
+ `/projects/${enc(project)}/collections/${enc(key)}/translations`,
346
+ void 0,
347
+ signal
348
+ ),
343
349
  diff: (project, collections) => this.mutate("POST", `/projects/${enc(project)}/schema/diff`, { collections }),
344
350
  push: (project, collections, opts = {}) => this.mutate("POST", `/projects/${enc(project)}/schema/push`, {
345
351
  collections,
@@ -358,7 +364,8 @@ var ManagementClient = class {
358
364
  entries = {
359
365
  list: (project, opts = {}) => this.page(`/projects/${enc(project)}/entries`, opts, {
360
366
  collection: opts.collection,
361
- status: opts.status
367
+ status: opts.status,
368
+ q: opts.q
362
369
  }),
363
370
  create: (project, body) => this.mutate("POST", `/projects/${enc(project)}/entries`, body),
364
371
  get: (project, entry, signal) => this.get(`/projects/${enc(project)}/entries/${enc(entry)}`, void 0, signal),
@@ -438,7 +445,25 @@ var ManagementClient = class {
438
445
  void 0,
439
446
  signal
440
447
  ),
441
- runChecks: (project, changeSet) => this.mutate("POST", `/projects/${enc(project)}/change-sets/${enc(changeSet)}/checks/run`)
448
+ runChecks: (project, changeSet) => this.mutate("POST", `/projects/${enc(project)}/change-sets/${enc(changeSet)}/checks/run`),
449
+ /** Report a verdict Myna cannot compute, against the current contents. */
450
+ reportCheck: (project, changeSet, name, body) => this.mutate(
451
+ "POST",
452
+ `/projects/${enc(project)}/change-sets/${enc(changeSet)}/checks/${enc(name)}`,
453
+ body
454
+ ),
455
+ /** Field-level before/after for everything this change set would change. */
456
+ diff: (project, changeSet, signal) => this.get(
457
+ `/projects/${enc(project)}/change-sets/${enc(changeSet)}/diff`,
458
+ void 0,
459
+ signal
460
+ )
461
+ };
462
+ // --- Declared external checks ---------------------------------------------
463
+ checks = {
464
+ list: (project, signal) => this.get(`/projects/${enc(project)}/checks`, void 0, signal),
465
+ declare: (project, body) => this.mutate("POST", `/projects/${enc(project)}/checks`, body),
466
+ remove: (project, name) => this.mutate("DELETE", `/projects/${enc(project)}/checks/${enc(name)}`)
442
467
  };
443
468
  // --- Previews -------------------------------------------------------------
444
469
  previews = {
@@ -452,7 +477,11 @@ var ManagementClient = class {
452
477
  completeUpload: (project, upload) => this.mutate("POST", `/projects/${enc(project)}/assets/uploads/${enc(upload)}/complete`).then(
453
478
  (r) => r.asset
454
479
  ),
455
- list: (project, opts = {}) => this.page(`/projects/${enc(project)}/assets`, opts),
480
+ list: (project, opts = {}) => this.page(`/projects/${enc(project)}/assets`, opts, {
481
+ tag: opts.tag,
482
+ checksum: opts.checksum,
483
+ orphaned: opts.orphaned ? "true" : void 0
484
+ }),
456
485
  get: (project, asset, signal) => this.get(`/projects/${enc(project)}/assets/${enc(asset)}`, void 0, signal),
457
486
  usage: (project, asset, signal) => this.get(`/projects/${enc(project)}/assets/${enc(asset)}`, { usage: "true" }, signal),
458
487
  update: (project, asset, body) => this.mutate("PATCH", `/projects/${enc(project)}/assets/${enc(asset)}`, body),
@@ -704,7 +733,10 @@ function loadConfigFile(env) {
704
733
  import { z } from "zod";
705
734
  function ok(text, structured) {
706
735
  return {
707
- content: [{ type: "text", text }],
736
+ content: [
737
+ { type: "text", text },
738
+ { type: "text", text: JSON.stringify(structured) }
739
+ ],
708
740
  structuredContent: structured
709
741
  };
710
742
  }
@@ -1273,6 +1305,387 @@ function registerTools(server, registry, options = { allowPathUploads: true }) {
1273
1305
  }
1274
1306
  }
1275
1307
  );
1308
+ server.registerTool(
1309
+ "myna_list_change_sets",
1310
+ {
1311
+ title: "List change sets",
1312
+ description: "List change sets in a project, newest first, optionally filtered by status (open, ready, published, closed). Use this to find work already in progress before starting a new change set.",
1313
+ inputSchema: {
1314
+ ...projectArg,
1315
+ status: z.enum(["open", "ready", "published", "closed"]).optional(),
1316
+ limit: z.number().int().min(1).max(100).optional(),
1317
+ cursor: z.string().optional()
1318
+ },
1319
+ annotations: { readOnlyHint: true, openWorldHint: true }
1320
+ },
1321
+ async (args) => {
1322
+ try {
1323
+ const { client, project } = registry.clientFor(args.project);
1324
+ const page = await client.changeSets.list(project, {
1325
+ ...args.status ? { status: args.status } : {},
1326
+ ...args.limit ? { limit: args.limit } : {},
1327
+ ...args.cursor ? { cursor: args.cursor } : {}
1328
+ });
1329
+ return ok(`${page.data.length} change set(s).`, page);
1330
+ } catch (error) {
1331
+ return fail(error);
1332
+ }
1333
+ }
1334
+ );
1335
+ server.registerTool(
1336
+ "myna_diff_change_set",
1337
+ {
1338
+ title: "Diff a change set",
1339
+ description: "Show what a change set would change, field by field: per item, the operation, the collection and slug, and every changed field with its before and after value. This is what a reviewer reads; use it to summarize your own work or to review someone else's.",
1340
+ inputSchema: { ...projectArg, changeSet: z.string() },
1341
+ annotations: { readOnlyHint: true, openWorldHint: true }
1342
+ },
1343
+ async (args) => {
1344
+ try {
1345
+ const { client, project } = registry.clientFor(args.project);
1346
+ const diff = await client.changeSets.diff(project, args.changeSet);
1347
+ const fields = diff.items.reduce((n, i) => n + i.changes.length, 0);
1348
+ return ok(`${diff.items.length} item(s), ${fields} changed field(s).`, diff);
1349
+ } catch (error) {
1350
+ return fail(error);
1351
+ }
1352
+ }
1353
+ );
1354
+ server.registerTool(
1355
+ "myna_update_change_set",
1356
+ {
1357
+ title: "Update a change set",
1358
+ description: "Rename a change set, change its description, or mark it ready for review. Marking it ready signals that a human is needed and emits change_set.ready_for_review.",
1359
+ inputSchema: {
1360
+ ...projectArg,
1361
+ changeSet: z.string(),
1362
+ title: z.string().min(1).max(200).optional(),
1363
+ description: z.string().max(2e3).optional(),
1364
+ status: z.enum(["open", "ready"]).optional()
1365
+ },
1366
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1367
+ },
1368
+ async (args) => {
1369
+ try {
1370
+ const { client, project } = registry.clientFor(args.project);
1371
+ const cs = await client.changeSets.update(project, args.changeSet, {
1372
+ ...args.title ? { title: args.title } : {},
1373
+ ...args.description ? { description: args.description } : {},
1374
+ ...args.status ? { status: args.status } : {}
1375
+ });
1376
+ return ok(`Change set ${cs.id} is ${cs.status}.`, { changeSet: cs });
1377
+ } catch (error) {
1378
+ return fail(error);
1379
+ }
1380
+ }
1381
+ );
1382
+ server.registerTool(
1383
+ "myna_close_change_set",
1384
+ {
1385
+ title: "Close a change set",
1386
+ description: "Abandon a change set without publishing it. The staged drafts and their revisions remain in history; the set simply stops being work in progress. Use this when a task is cancelled, so open sets do not accumulate. Requires confirm=true.",
1387
+ inputSchema: { ...projectArg, changeSet: z.string(), confirm: z.literal(true).describe("Must be true.") },
1388
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1389
+ },
1390
+ async (args) => {
1391
+ try {
1392
+ const { client, project } = registry.clientFor(args.project);
1393
+ const cs = await client.changeSets.close(project, args.changeSet);
1394
+ return ok(`Closed ${cs.id}.`, { changeSet: cs });
1395
+ } catch (error) {
1396
+ return fail(error);
1397
+ }
1398
+ }
1399
+ );
1400
+ server.registerTool(
1401
+ "myna_request_change_set_review",
1402
+ {
1403
+ title: "Request review",
1404
+ description: "Ask a specific person to review a change set. Pass their user id. Requires content:review scope.",
1405
+ inputSchema: {
1406
+ ...projectArg,
1407
+ changeSet: z.string(),
1408
+ reviewerId: z.string().describe("User id (usr_) to assign as reviewer.")
1409
+ },
1410
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1411
+ },
1412
+ async (args) => {
1413
+ try {
1414
+ const { client, project } = registry.clientFor(args.project);
1415
+ const reviews = await client.changeSets.requestReview(project, args.changeSet, {
1416
+ reviewerType: "user",
1417
+ reviewerId: args.reviewerId
1418
+ });
1419
+ return ok(`${reviews.length} reviewer(s) assigned.`, { reviews });
1420
+ } catch (error) {
1421
+ return fail(error);
1422
+ }
1423
+ }
1424
+ );
1425
+ server.registerTool(
1426
+ "myna_report_check_result",
1427
+ {
1428
+ title: "Report an external check result",
1429
+ description: "Report the verdict of a check Myna cannot compute itself \u2014 a link check, a house-style review, an external build. The check must have been declared on the project first. The result is recorded against the change set's current contents, so later edits mark it stale. A failing required check blocks publish. Requires content:review scope.",
1430
+ inputSchema: {
1431
+ ...projectArg,
1432
+ changeSet: z.string(),
1433
+ name: z.string().describe("The declared check name, e.g. links."),
1434
+ status: z.enum(["passed", "failed", "skipped"]),
1435
+ details: z.array(
1436
+ z.object({
1437
+ resourceId: z.string().optional(),
1438
+ path: z.string().optional(),
1439
+ message: z.string()
1440
+ })
1441
+ ).optional().describe("What the check found; shown to the reviewer.")
1442
+ },
1443
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1444
+ },
1445
+ async (args) => {
1446
+ try {
1447
+ const { client, project } = registry.clientFor(args.project);
1448
+ const check = await client.changeSets.reportCheck(project, args.changeSet, args.name, {
1449
+ status: args.status,
1450
+ ...args.details ? { details: args.details } : {}
1451
+ });
1452
+ return ok(`Reported ${args.name}: ${check.status}.`, { check });
1453
+ } catch (error) {
1454
+ return fail(error);
1455
+ }
1456
+ }
1457
+ );
1458
+ server.registerTool(
1459
+ "myna_search_entries",
1460
+ {
1461
+ title: "Search entries",
1462
+ description: 'Full-text search across every text field of an entry, matching either its draft or its published state. Use this to find the entry a human described in prose ("the pricing page", "the post about webhooks") instead of listing a collection and scanning it.',
1463
+ inputSchema: {
1464
+ ...projectArg,
1465
+ query: z.string().min(1).describe("Words to search for. Supports quoted phrases and -exclusions."),
1466
+ collection: z.string().optional().describe("Restrict to one collection."),
1467
+ limit: z.number().int().min(1).max(100).optional()
1468
+ },
1469
+ annotations: { readOnlyHint: true, openWorldHint: true }
1470
+ },
1471
+ async (args) => {
1472
+ try {
1473
+ const { client, project } = registry.clientFor(args.project);
1474
+ const page = await client.entries.list(project, {
1475
+ q: args.query,
1476
+ ...args.collection ? { collection: args.collection } : {},
1477
+ limit: args.limit ?? 20
1478
+ });
1479
+ return ok(`${page.data.length} match(es) for "${args.query}".`, page);
1480
+ } catch (error) {
1481
+ return fail(error);
1482
+ }
1483
+ }
1484
+ );
1485
+ server.registerTool(
1486
+ "myna_unpublish_entry",
1487
+ {
1488
+ title: "Unpublish an entry",
1489
+ description: "Stage the removal of an entry from published content. Like every write this is a draft operation: it takes effect when the change set is published. The entry and its history are kept. Requires confirm=true.",
1490
+ inputSchema: {
1491
+ ...projectArg,
1492
+ entry: z.string(),
1493
+ changeSet: z.string().optional(),
1494
+ confirm: z.literal(true).describe("Must be true.")
1495
+ },
1496
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1497
+ },
1498
+ async (args) => {
1499
+ try {
1500
+ const { client, project } = registry.clientFor(args.project);
1501
+ const id = await resolveEntryId(client, project, args.entry);
1502
+ const result = await client.entries.unpublish(project, id, args.changeSet);
1503
+ return ok(`Unpublish staged on ${result.changeSetId}.`, result);
1504
+ } catch (error) {
1505
+ return fail(error);
1506
+ }
1507
+ }
1508
+ );
1509
+ server.registerTool(
1510
+ "myna_duplicate_entry",
1511
+ {
1512
+ title: "Duplicate an entry",
1513
+ description: "Copy an entry into a new draft with a fresh slug. Useful as a starting point when a new entry should follow the shape of an existing one.",
1514
+ inputSchema: { ...projectArg, entry: z.string(), changeSet: z.string().optional() },
1515
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1516
+ },
1517
+ async (args) => {
1518
+ try {
1519
+ const { client, project } = registry.clientFor(args.project);
1520
+ const id = await resolveEntryId(client, project, args.entry);
1521
+ const entry = await client.entries.duplicate(project, id, args.changeSet);
1522
+ return ok(`Created ${entry.id} (${entry.slug ?? "no slug"}).`, { entry });
1523
+ } catch (error) {
1524
+ return fail(error);
1525
+ }
1526
+ }
1527
+ );
1528
+ server.registerTool(
1529
+ "myna_import_entries",
1530
+ {
1531
+ title: "Import entries",
1532
+ description: "Create or update many entries in one collection in a single call, matched by slug: rows that already exist are updated, identical rows are skipped. Use dryRun=true first to see what would happen. Everything lands as drafts on one change set.",
1533
+ inputSchema: {
1534
+ ...projectArg,
1535
+ collection: z.string(),
1536
+ entries: z.array(z.object({ slug: z.string().optional(), data: z.record(z.string(), z.unknown()) })).min(1).max(500),
1537
+ changeSet: z.string().optional(),
1538
+ changeSummary: z.string().optional(),
1539
+ dryRun: z.boolean().optional().describe("Report what would change without writing.")
1540
+ },
1541
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1542
+ },
1543
+ async (args) => {
1544
+ try {
1545
+ const { client, project } = registry.clientFor(args.project);
1546
+ const result = await client.entries.import(project, {
1547
+ collection: args.collection,
1548
+ entries: args.entries,
1549
+ ...args.changeSet ? { changeSetId: args.changeSet } : {},
1550
+ ...args.changeSummary ? { changeSummary: args.changeSummary } : {},
1551
+ ...args.dryRun !== void 0 ? { dryRun: args.dryRun } : {}
1552
+ });
1553
+ const { created, updated, unchanged, invalid } = result.summary;
1554
+ return ok(
1555
+ `${args.dryRun ? "Would import" : "Imported"}: ${created} created, ${updated} updated, ${unchanged} unchanged, ${invalid} invalid.`,
1556
+ result
1557
+ );
1558
+ } catch (error) {
1559
+ return fail(error);
1560
+ }
1561
+ }
1562
+ );
1563
+ server.registerTool(
1564
+ "myna_reorder_entries",
1565
+ {
1566
+ title: "Reorder a collection",
1567
+ description: "Set the editorial order of a collection. Unlike every other write, ordering is arrangement rather than content: it carries no revision, is not staged on a change set, and reaches readers immediately \u2014 which is why confirm=true is required. Never use it to change what an entry says.",
1568
+ inputSchema: {
1569
+ ...projectArg,
1570
+ collection: z.string(),
1571
+ order: z.array(z.string()).optional().describe("Entry ids in the intended order."),
1572
+ move: z.object({ entry: z.string(), before: z.string().optional(), after: z.string().optional() }).optional().describe("Move one entry relative to another, instead of listing the whole order."),
1573
+ confirm: z.literal(true).describe("Must be true: this is immediately visible to readers.")
1574
+ },
1575
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1576
+ },
1577
+ async (args) => {
1578
+ try {
1579
+ if (Boolean(args.order) === Boolean(args.move)) {
1580
+ throw new Error("Provide exactly one of `order` or `move`.");
1581
+ }
1582
+ const { client, project } = registry.clientFor(args.project);
1583
+ const result = await client.entries.reorder(project, args.collection, {
1584
+ ...args.order ? { order: args.order } : {},
1585
+ ...args.move ? { move: args.move } : {}
1586
+ });
1587
+ return ok(`Reordered with ${result.writes} write(s).`, result);
1588
+ } catch (error) {
1589
+ return fail(error);
1590
+ }
1591
+ }
1592
+ );
1593
+ server.registerTool(
1594
+ "myna_list_assets",
1595
+ {
1596
+ title: "List assets",
1597
+ description: "List a project's assets, optionally filtered by tag or checksum. Check here before uploading: an asset that already exists should be referenced, not uploaded again.",
1598
+ inputSchema: {
1599
+ ...projectArg,
1600
+ tag: z.string().optional(),
1601
+ checksum: z.string().optional().describe("MD5 hex; finds an identical file already uploaded."),
1602
+ limit: z.number().int().min(1).max(100).optional(),
1603
+ cursor: z.string().optional()
1604
+ },
1605
+ annotations: { readOnlyHint: true, openWorldHint: true }
1606
+ },
1607
+ async (args) => {
1608
+ try {
1609
+ const { client, project } = registry.clientFor(args.project);
1610
+ const page = await client.assets.list(project, {
1611
+ ...args.tag ? { tag: args.tag } : {},
1612
+ ...args.checksum ? { checksum: args.checksum } : {},
1613
+ ...args.limit ? { limit: args.limit } : {},
1614
+ ...args.cursor ? { cursor: args.cursor } : {}
1615
+ });
1616
+ return ok(`${page.data.length} asset(s).`, page);
1617
+ } catch (error) {
1618
+ return fail(error);
1619
+ }
1620
+ }
1621
+ );
1622
+ server.registerTool(
1623
+ "myna_get_asset",
1624
+ {
1625
+ title: "Get an asset",
1626
+ description: "Fetch one asset: its delivery URL, content type, byte size, pixel dimensions, alt text, and tags.",
1627
+ inputSchema: { ...projectArg, asset: z.string().describe("Asset id (ast_).") },
1628
+ annotations: { readOnlyHint: true, openWorldHint: true }
1629
+ },
1630
+ async (args) => {
1631
+ try {
1632
+ const { client, project } = registry.clientFor(args.project);
1633
+ const asset = await client.assets.get(project, args.asset);
1634
+ return ok(`${asset.displayFilename} (${asset.contentType}).`, { asset });
1635
+ } catch (error) {
1636
+ return fail(error);
1637
+ }
1638
+ }
1639
+ );
1640
+ server.registerTool(
1641
+ "myna_update_asset",
1642
+ {
1643
+ title: "Update asset metadata",
1644
+ description: "Set an asset's alt text, caption, display filename, or tags. Alt text in particular is what the built-in alt-text check looks for, so filling it in here clears that check.",
1645
+ inputSchema: {
1646
+ ...projectArg,
1647
+ asset: z.string(),
1648
+ defaultAlt: z.string().optional(),
1649
+ caption: z.string().optional(),
1650
+ displayFilename: z.string().optional(),
1651
+ tags: z.array(z.string()).optional()
1652
+ },
1653
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1654
+ },
1655
+ async (args) => {
1656
+ try {
1657
+ const { client, project } = registry.clientFor(args.project);
1658
+ const asset = await client.assets.update(project, args.asset, {
1659
+ ...args.defaultAlt !== void 0 ? { defaultAlt: args.defaultAlt } : {},
1660
+ ...args.caption !== void 0 ? { caption: args.caption } : {},
1661
+ ...args.displayFilename !== void 0 ? { displayFilename: args.displayFilename } : {},
1662
+ ...args.tags !== void 0 ? { tags: args.tags } : {}
1663
+ });
1664
+ return ok(`Updated ${asset.id}.`, { asset });
1665
+ } catch (error) {
1666
+ return fail(error);
1667
+ }
1668
+ }
1669
+ );
1670
+ server.registerTool(
1671
+ "myna_translation_coverage",
1672
+ {
1673
+ title: "Translation coverage",
1674
+ description: "Report what is still untranslated in a collection, per project locale: how complete each locale is, which fields are missing most often, and a worklist of entries to fix. Empty when the project has no locales or the collection has no localized fields.",
1675
+ inputSchema: { ...projectArg, collection: z.string() },
1676
+ annotations: { readOnlyHint: true, openWorldHint: true }
1677
+ },
1678
+ async (args) => {
1679
+ try {
1680
+ const { client, project } = registry.clientFor(args.project);
1681
+ const report = await client.schema.translations(project, args.collection);
1682
+ const summary = report.coverage.map((c) => `${c.locale} ${c.percent}%`).join(", ");
1683
+ return ok(summary || "No localized fields.", report);
1684
+ } catch (error) {
1685
+ return fail(error);
1686
+ }
1687
+ }
1688
+ );
1276
1689
  }
1277
1690
 
1278
1691
  // src/resources.ts
@@ -1286,7 +1699,7 @@ function registerResources(server, registry) {
1286
1699
  "myna://projects",
1287
1700
  { title: "Projects", description: "Projects in the default organization.", mimeType: "application/json" },
1288
1701
  async (uri) => {
1289
- const { client } = registry.clientFor();
1702
+ const { client } = registry.clientForOrg();
1290
1703
  const org = registry.organizationFor();
1291
1704
  const projects = await client.projects.list(org);
1292
1705
  return json(uri.href, { organization: org, projects });
@@ -1347,7 +1760,7 @@ function registerResources(server, registry) {
1347
1760
  }
1348
1761
 
1349
1762
  // src/version.ts
1350
- var VERSION = true ? "0.4.0" : "0.0.0-dev";
1763
+ var VERSION = true ? "0.5.0" : "0.0.0-dev";
1351
1764
 
1352
1765
  // src/server.ts
1353
1766
  var SERVER_NAME = "myna";