@ampless/mcp-server 0.2.0-alpha.8 → 0.2.0-alpha.9

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.
@@ -526,11 +526,379 @@ function getSchema() {
526
526
  editorTrust: 'editor stores arbitrary HTML/JS verbatim \u2014 same trust shape as WordPress unfiltered_html capability. See docs/architecture/04-access-layer-mcp.md \xA7"editor \u306E\u4FE1\u983C\u30E2\u30C7\u30EB".',
527
527
  tiptapBody: 'When format=tiptap, body is the tiptap document JSON: { type: "doc", content: [...] }. The renderer expects this shape.',
528
528
  noLayout: "metadata.no_layout=true serves the post as bare HTML with no theme chrome \u2014 the public route at /<slug> 302-redirects to /raw/<slug>, and that route renders the body verbatim with no wrapping <html>/<head>/layout. Use this for landing pages, embeds, or any post whose body is a full HTML document. Only meaningful with format=html (the other formats need the theme renderer).",
529
- staticFormat: "A fourth format value `static` exists on the underlying data model for posts whose body is a JSON manifest pointing to a pre-uploaded HTML/CSS/JS bundle in S3 at public/static/<siteId>/<slug>/. Static uploads currently only flow through the admin UI; the MCP `upload_media` tool writes to public/media/ and does not handle static bundles. Use the admin StaticUploader for now."
529
+ staticFormat: "A fourth format value `static` exists on the underlying data model for posts whose body is a JSON manifest pointing to a pre-uploaded HTML/CSS/JS bundle in S3 at public/static/<siteId>/<slug>/. Static posts are created/edited through the dedicated tools `upload_static_bundle` (zip in one shot), `upload_static_file` / `delete_static_file` (incremental per-file ops), and `commit_static_post` (rebuild the manifest from the current S3 prefix). `create_post` / `update_post` intentionally do NOT accept format=static \u2014 the bundle tools are the only supported entry point so the Post manifest stays in sync with the S3 prefix."
530
530
  }
531
531
  };
532
532
  }
533
533
 
534
+ // src/tools/upload-static-bundle.ts
535
+ import {
536
+ bundlePrefix,
537
+ mimeTypeFor,
538
+ pickDefaultEntrypoint,
539
+ validateBundle
540
+ } from "ampless";
541
+
542
+ // src/tools/static-bundle-extract.ts
543
+ import { unzipSync, strFromU8 } from "fflate";
544
+ import {
545
+ validateBundlePath,
546
+ stripCommonPrefix
547
+ } from "ampless";
548
+ var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
549
+ function extractZipFromBuffer(buffer, opts = {}) {
550
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
551
+ const entries2 = unzipSync(buffer);
552
+ const files = [];
553
+ const issues = [];
554
+ let totalBytes = 0;
555
+ for (const [name, data] of Object.entries(entries2)) {
556
+ if (name.endsWith("/")) continue;
557
+ const reason = validateBundlePath(name);
558
+ if (reason) {
559
+ const silent = reason === "macOS resource fork" || reason === ".DS_Store junk" || reason === "Thumbs.db junk" || reason === "directory entry";
560
+ if (!silent) issues.push({ path: name, reason });
561
+ continue;
562
+ }
563
+ totalBytes += data.byteLength;
564
+ if (totalBytes > maxBytes) {
565
+ throw new Error(
566
+ `Bundle too large: uncompressed size exceeds the ${Math.round(maxBytes / 1024 / 1024)} MB ceiling.`
567
+ );
568
+ }
569
+ files.push({ path: name, data });
570
+ }
571
+ return { files: stripCommonPrefix(files), issues, totalBytes };
572
+ }
573
+ function decodeUtf8(data) {
574
+ return strFromU8(data);
575
+ }
576
+
577
+ // src/tools/upsert-static-post.ts
578
+ import {
579
+ composeSiteIdStatus as composeSiteIdStatus3,
580
+ composeSiteIdSlug as composeSiteIdSlug3,
581
+ encodeAwsJson as encodeAwsJson3
582
+ } from "ampless";
583
+ var CREATE_MUTATION = (
584
+ /* GraphQL */
585
+ `
586
+ ${POST_FIELDS}
587
+ mutation CreatePost($input: CreatePostInput!) {
588
+ createPost(input: $input) {
589
+ ...PostFields
590
+ }
591
+ }
592
+ `
593
+ );
594
+ var UPDATE_MUTATION = (
595
+ /* GraphQL */
596
+ `
597
+ ${POST_FIELDS}
598
+ mutation UpdatePost($input: UpdatePostInput!) {
599
+ updatePost(input: $input) {
600
+ ...PostFields
601
+ }
602
+ }
603
+ `
604
+ );
605
+ async function upsertStaticPost(graphql, siteId, slug, body, fields) {
606
+ const existing = await getPost(graphql, siteId, { siteId, slug });
607
+ if (existing) {
608
+ const input2 = {
609
+ siteId,
610
+ postId: existing.postId,
611
+ format: "static",
612
+ body: encodeAwsJson3(body)
613
+ };
614
+ if (fields.title !== void 0) input2.title = fields.title;
615
+ if (fields.excerpt !== void 0) input2.excerpt = fields.excerpt;
616
+ if (fields.status !== void 0) {
617
+ input2.status = fields.status;
618
+ input2.siteIdStatus = composeSiteIdStatus3(siteId, fields.status);
619
+ }
620
+ if (fields.publishedAt !== void 0) input2.publishedAt = fields.publishedAt;
621
+ if (fields.tags !== void 0) input2.tags = fields.tags;
622
+ if (fields.metadata !== void 0) {
623
+ input2.metadata = encodeAwsJson3(fields.metadata);
624
+ }
625
+ const data2 = await graphql.query(UPDATE_MUTATION, { input: input2 });
626
+ const updated = toCorePost(data2.updatePost);
627
+ await syncPostTags(graphql, updated, existing);
628
+ return { post: updated, created: false };
629
+ }
630
+ if (!fields.title) {
631
+ throw new Error(
632
+ `commit_static_post: cannot create a new post at slug "${slug}" without a title. Pass \`title\` or commit against an existing post.`
633
+ );
634
+ }
635
+ const status = fields.status ?? "draft";
636
+ const publishedAt = fields.publishedAt ?? (status === "published" ? (/* @__PURE__ */ new Date()).toISOString() : void 0);
637
+ const postId = fields.postId ?? `post-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
638
+ const input = {
639
+ siteId,
640
+ postId,
641
+ slug,
642
+ title: fields.title,
643
+ format: "static",
644
+ body: encodeAwsJson3(body),
645
+ status,
646
+ siteIdStatus: composeSiteIdStatus3(siteId, status),
647
+ siteIdSlug: composeSiteIdSlug3(siteId, slug)
648
+ };
649
+ if (fields.excerpt !== void 0) input.excerpt = fields.excerpt;
650
+ if (publishedAt !== void 0) input.publishedAt = publishedAt;
651
+ if (fields.tags !== void 0) input.tags = fields.tags;
652
+ if (fields.metadata !== void 0) input.metadata = encodeAwsJson3(fields.metadata);
653
+ const data = await graphql.query(CREATE_MUTATION, { input });
654
+ const created = toCorePost(data.createPost);
655
+ await syncPostTags(graphql, created, null);
656
+ return { post: created, created: true };
657
+ }
658
+
659
+ // src/tools/upload-static-bundle.ts
660
+ var uploadStaticBundleSchema = {
661
+ type: "object",
662
+ required: ["slug", "title", "zipBase64"],
663
+ properties: {
664
+ siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
665
+ postId: {
666
+ type: "string",
667
+ description: "Optional explicit Post id when creating a new post. Ignored if a post at this slug already exists."
668
+ },
669
+ slug: { type: "string", description: "URL slug. Doubles as the S3 bundle path key." },
670
+ title: { type: "string" },
671
+ zipBase64: {
672
+ type: "string",
673
+ description: "Base64-encoded zip archive. NO `data:` URL prefix. Every entry must reference others by RELATIVE path; absolute paths (`/foo`) and protocol-relative URLs (`//cdn/foo`) inside HTML / CSS / SVG are rejected as the bundle would not be portable across URL prefixes."
674
+ },
675
+ entrypoint: {
676
+ type: "string",
677
+ description: "Relative path to the file served at the post URL (default `index.html`, or the first `.html` at root if `index.html` is absent)."
678
+ },
679
+ status: { type: "string", enum: ["draft", "published"], default: "draft" },
680
+ excerpt: { type: "string" },
681
+ publishedAt: { type: "string", description: 'ISO 8601 timestamp; defaults to "now" when status=published.' },
682
+ tags: { type: "array", items: { type: "string" } },
683
+ metadata: {
684
+ type: "object",
685
+ description: "Free-form per-post metadata. NOTE: `no_layout` is irrelevant for static posts (the bundle is already served raw).",
686
+ additionalProperties: true
687
+ }
688
+ }
689
+ };
690
+ async function uploadStaticBundle(graphql, storage, defaultSiteId, args) {
691
+ const siteId = args.siteId ?? defaultSiteId;
692
+ const slug = args.slug;
693
+ const zipBytes = Buffer.from(args.zipBase64, "base64");
694
+ if (zipBytes.length === 0) {
695
+ throw new Error("upload_static_bundle: zipBase64 decoded to zero bytes.");
696
+ }
697
+ const { files, issues } = extractZipFromBuffer(zipBytes);
698
+ if (issues.length > 0) {
699
+ throw new Error(
700
+ `upload_static_bundle: rejected bundle path(s): ${issues.map((i) => `${i.path} (${i.reason})`).join("; ")}`
701
+ );
702
+ }
703
+ if (files.length === 0) {
704
+ throw new Error("upload_static_bundle: zip contained no files.");
705
+ }
706
+ const contentIssues = validateBundle(files);
707
+ if (contentIssues.length > 0) {
708
+ throw new Error(
709
+ `upload_static_bundle: bundle contains absolute / protocol-relative refs: ${contentIssues.map((i) => `${i.path}: ${i.reason}`).join("; ")}`
710
+ );
711
+ }
712
+ const entrypoint = args.entrypoint ?? pickDefaultEntrypoint(files);
713
+ if (!files.some((f) => f.path === entrypoint)) {
714
+ throw new Error(
715
+ `upload_static_bundle: entrypoint "${entrypoint}" is not present in the bundle.`
716
+ );
717
+ }
718
+ const prefix = bundlePrefix(siteId, slug);
719
+ const existing = await storage.listObjects(prefix).catch((err) => {
720
+ console.error("[upload_static_bundle] listObjects failed (proceeding)", err);
721
+ return [];
722
+ });
723
+ for (const obj of existing) {
724
+ await storage.deleteObject(obj.key);
725
+ }
726
+ let uploadedFiles = 0;
727
+ for (const f of files) {
728
+ await storage.putObject(`${prefix}${f.path}`, f.data, mimeTypeFor(f.path));
729
+ uploadedFiles += 1;
730
+ }
731
+ const body = {
732
+ entrypoint,
733
+ files: files.map((f) => f.path).sort(),
734
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
735
+ };
736
+ const { post } = await upsertStaticPost(graphql, siteId, slug, body, {
737
+ title: args.title,
738
+ postId: args.postId,
739
+ status: args.status,
740
+ publishedAt: args.publishedAt,
741
+ excerpt: args.excerpt,
742
+ tags: args.tags,
743
+ metadata: args.metadata
744
+ });
745
+ return { post, bundle: body, uploadedFiles };
746
+ }
747
+
748
+ // src/tools/upload-static-file.ts
749
+ import {
750
+ bundlePrefix as bundlePrefix2,
751
+ findAbsolutePathRefs,
752
+ mimeTypeFor as mimeTypeFor2,
753
+ validateBundlePath as validateBundlePath2,
754
+ TEXT_EXTENSIONS
755
+ } from "ampless";
756
+ var uploadStaticFileSchema = {
757
+ type: "object",
758
+ required: ["slug", "filename", "base64Data"],
759
+ properties: {
760
+ siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
761
+ slug: { type: "string", description: "Bundle slug. Files land at public/static/<siteId>/<slug>/<filename>." },
762
+ filename: {
763
+ type: "string",
764
+ description: "Relative path inside the bundle. Must NOT start with `/`, contain `..`, or include null bytes. macOS / Windows zip junk (`.DS_Store`, `__MACOSX/*`, `Thumbs.db`) is also rejected."
765
+ },
766
+ contentType: {
767
+ type: "string",
768
+ description: "IANA media type. Omit to derive from the filename extension (text/html, text/css, application/javascript, image/* \u2026; falls back to application/octet-stream)."
769
+ },
770
+ base64Data: {
771
+ type: "string",
772
+ description: "Base64-encoded file bytes. NO `data:` URL prefix. Text files (HTML/CSS/SVG) are linted for absolute / protocol-relative URL refs \u2014 failures throw."
773
+ }
774
+ }
775
+ };
776
+ async function uploadStaticFile(storage, defaultSiteId, args) {
777
+ const siteId = args.siteId ?? defaultSiteId;
778
+ const filename = args.filename;
779
+ const reason = validateBundlePath2(filename);
780
+ if (reason) {
781
+ throw new Error(`upload_static_file: invalid filename "${filename}" (${reason})`);
782
+ }
783
+ const body = Buffer.from(args.base64Data, "base64");
784
+ if (body.length === 0) {
785
+ throw new Error("upload_static_file: base64Data decoded to zero bytes.");
786
+ }
787
+ const ext = filename.toLowerCase().slice(filename.lastIndexOf("."));
788
+ if (TEXT_EXTENSIONS.has(ext)) {
789
+ const text = body.toString("utf8");
790
+ const issues = findAbsolutePathRefs(filename, text);
791
+ if (issues.length > 0) {
792
+ throw new Error(
793
+ `upload_static_file: ${filename} contains absolute / protocol-relative refs: ${issues.map((i) => i.reason).join("; ")}`
794
+ );
795
+ }
796
+ }
797
+ const contentType = args.contentType ?? mimeTypeFor2(filename);
798
+ const key = `${bundlePrefix2(siteId, args.slug)}${filename}`;
799
+ const url = await storage.putObject(key, body, contentType);
800
+ return { key, url, size: body.length };
801
+ }
802
+
803
+ // src/tools/delete-static-file.ts
804
+ import { bundlePrefix as bundlePrefix3, validateBundlePath as validateBundlePath3 } from "ampless";
805
+ var deleteStaticFileSchema = {
806
+ type: "object",
807
+ required: ["slug", "filename"],
808
+ properties: {
809
+ siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
810
+ slug: { type: "string" },
811
+ filename: {
812
+ type: "string",
813
+ description: "Relative path inside the bundle to delete. Same path rules as `upload_static_file` apply (no `..`, no absolute paths, no null bytes)."
814
+ }
815
+ }
816
+ };
817
+ async function deleteStaticFile(storage, defaultSiteId, args) {
818
+ const siteId = args.siteId ?? defaultSiteId;
819
+ const reason = validateBundlePath3(args.filename);
820
+ if (reason) {
821
+ throw new Error(`delete_static_file: invalid filename "${args.filename}" (${reason})`);
822
+ }
823
+ const prefix = bundlePrefix3(siteId, args.slug);
824
+ const key = `${prefix}${args.filename}`;
825
+ const existing = await storage.listObjects(key);
826
+ const found = existing.some((o) => o.key === key);
827
+ if (!found) {
828
+ return { key, deleted: false };
829
+ }
830
+ await storage.deleteObject(key);
831
+ return { key, deleted: true };
832
+ }
833
+
834
+ // src/tools/commit-static-post.ts
835
+ import {
836
+ bundlePrefix as bundlePrefix4,
837
+ pickDefaultEntrypoint as pickDefaultEntrypoint2
838
+ } from "ampless";
839
+ var commitStaticPostSchema = {
840
+ type: "object",
841
+ required: ["slug"],
842
+ properties: {
843
+ siteId: { type: "string", description: 'Site identifier (defaults to "default")' },
844
+ slug: { type: "string" },
845
+ title: {
846
+ type: "string",
847
+ description: "Required when this is a brand-new post (no existing row at the slug). On update, falls back to the current title."
848
+ },
849
+ entrypoint: {
850
+ type: "string",
851
+ description: "Relative path of the entry file (defaults to `index.html`, or the first `.html` at root if absent). Must exist under the current S3 prefix."
852
+ },
853
+ postId: {
854
+ type: "string",
855
+ description: "Optional explicit Post id when creating a new post. Ignored if a row at this slug already exists."
856
+ },
857
+ status: { type: "string", enum: ["draft", "published"] },
858
+ publishedAt: { type: "string", description: "ISO 8601 timestamp." },
859
+ excerpt: { type: "string" },
860
+ tags: { type: "array", items: { type: "string" } },
861
+ metadata: {
862
+ type: "object",
863
+ description: "Free-form per-post metadata. Passing this REPLACES the existing object on update.",
864
+ additionalProperties: true
865
+ }
866
+ }
867
+ };
868
+ async function commitStaticPost(graphql, storage, defaultSiteId, args) {
869
+ const siteId = args.siteId ?? defaultSiteId;
870
+ const slug = args.slug;
871
+ const prefix = bundlePrefix4(siteId, slug);
872
+ const objects = await storage.listObjects(prefix);
873
+ if (objects.length === 0) {
874
+ throw new Error(
875
+ `commit_static_post: no files found under "${prefix}". Upload at least one file via upload_static_file or upload_static_bundle before committing.`
876
+ );
877
+ }
878
+ const relPaths = objects.map((o) => o.key.slice(prefix.length)).filter((p) => p !== "").sort();
879
+ const entrypoint = args.entrypoint ?? pickDefaultEntrypoint2(relPaths.map((p) => ({ path: p })));
880
+ if (!relPaths.includes(entrypoint)) {
881
+ throw new Error(
882
+ `commit_static_post: entrypoint "${entrypoint}" is not present under "${prefix}".`
883
+ );
884
+ }
885
+ const body = {
886
+ entrypoint,
887
+ files: relPaths,
888
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
889
+ };
890
+ const { post, created } = await upsertStaticPost(graphql, siteId, slug, body, {
891
+ title: args.title,
892
+ postId: args.postId,
893
+ excerpt: args.excerpt,
894
+ status: args.status,
895
+ publishedAt: args.publishedAt,
896
+ tags: args.tags,
897
+ metadata: args.metadata
898
+ });
899
+ return { post, bundle: body, created };
900
+ }
901
+
534
902
  // src/tools/index.ts
535
903
  var tools = [
536
904
  {
@@ -574,6 +942,48 @@ var tools = [
574
942
  description: "Returns the CMS content schema (Post/Page/Media field shapes, format enum, notes). Useful as the first call to understand what fields are available.",
575
943
  inputSchema: getSchemaSchema,
576
944
  handler: async () => getSchema()
945
+ },
946
+ {
947
+ name: "upload_static_bundle",
948
+ description: "Create or replace a `format: 'static'` post in one shot. Pass a base64-encoded zip; the server unpacks it, validates every path + lints HTML/CSS/SVG for absolute path refs (bundles must be self-contained via relative paths), wipes the existing S3 prefix at public/static/<siteId>/<slug>/, uploads every file, and upserts the Post row with a manifest pointing at the entrypoint. Use this when you have the whole bundle to submit at once. For incremental edits use upload_static_file / delete_static_file followed by commit_static_post.",
949
+ inputSchema: uploadStaticBundleSchema,
950
+ handler: (args, ctx) => uploadStaticBundle(
951
+ ctx.graphql,
952
+ ctx.storage(),
953
+ ctx.defaultSiteId,
954
+ args
955
+ )
956
+ },
957
+ {
958
+ name: "upload_static_file",
959
+ description: "Upload a single file into an existing static bundle's S3 prefix. The Post row is NOT modified \u2014 its `body` manifest will be out of sync with the prefix until you call `commit_static_post`. Use this for incremental edits (one CSS swap, single image change) where rebuilding the entire zip would be overkill. Text files (HTML/CSS/SVG) are linted for absolute / protocol-relative URL refs the same as the zip flow.",
960
+ inputSchema: uploadStaticFileSchema,
961
+ handler: (args, ctx) => uploadStaticFile(
962
+ ctx.storage(),
963
+ ctx.defaultSiteId,
964
+ args
965
+ )
966
+ },
967
+ {
968
+ name: "delete_static_file",
969
+ description: "Delete a single file from a static bundle's S3 prefix. The Post row is NOT modified \u2014 its `body` manifest will list the deleted file until you call `commit_static_post`. Idempotent: returns `{ deleted: false }` instead of throwing if the file isn't there.",
970
+ inputSchema: deleteStaticFileSchema,
971
+ handler: (args, ctx) => deleteStaticFile(
972
+ ctx.storage(),
973
+ ctx.defaultSiteId,
974
+ args
975
+ )
976
+ },
977
+ {
978
+ name: "commit_static_post",
979
+ description: 'Rebuild a static post\'s Post row from whatever is currently in its S3 prefix. Scans `public/static/<siteId>/<slug>/`, picks the entrypoint (default `index.html` or override), and upserts the Post with a fresh manifest (sorted file list + uploadedAt timestamp). Use this as the "save" step after a series of `upload_static_file` / `delete_static_file` calls. `title` is required when creating a brand-new post; on update, existing fields are preserved unless explicitly overridden.',
980
+ inputSchema: commitStaticPostSchema,
981
+ handler: (args, ctx) => commitStaticPost(
982
+ ctx.graphql,
983
+ ctx.storage(),
984
+ ctx.defaultSiteId,
985
+ args
986
+ )
577
987
  }
578
988
  ];
579
989
  async function dispatchToolCall(name, args, ctx) {
@@ -586,6 +996,8 @@ function getTools() {
586
996
  }
587
997
 
588
998
  export {
999
+ extractZipFromBuffer,
1000
+ decodeUtf8,
589
1001
  tools,
590
1002
  dispatchToolCall,
591
1003
  getTools
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-IW52OUIR.js";
8
8
  import {
9
9
  tools
10
- } from "./chunk-EGQ7Q3Y3.js";
10
+ } from "./chunk-4PTNHZO4.js";
11
11
  import {
12
12
  AwsCrc32,
13
13
  __awaiter,
@@ -32067,6 +32067,32 @@ function isValidBase64EncodedSSECustomerKey(str, options) {
32067
32067
  }
32068
32068
  }
32069
32069
 
32070
+ // ../../node_modules/.pnpm/@aws-sdk+client-s3@3.1048.0/node_modules/@aws-sdk/client-s3/dist-es/commands/DeleteObjectCommand.js
32071
+ var DeleteObjectCommand = class extends Command.classBuilder().ep({
32072
+ ...commonParams,
32073
+ Bucket: { type: "contextParams", name: "Bucket" },
32074
+ Key: { type: "contextParams", name: "Key" }
32075
+ }).m(function(Command2, cs, config2, o2) {
32076
+ return [
32077
+ getEndpointPlugin(config2, Command2.getEndpointParameterInstructions()),
32078
+ getThrow200ExceptionsPlugin(config2)
32079
+ ];
32080
+ }).s("AmazonS3", "DeleteObject", {}).n("S3Client", "DeleteObjectCommand").sc(DeleteObject$).build() {
32081
+ };
32082
+
32083
+ // ../../node_modules/.pnpm/@aws-sdk+client-s3@3.1048.0/node_modules/@aws-sdk/client-s3/dist-es/commands/ListObjectsV2Command.js
32084
+ var ListObjectsV2Command = class extends Command.classBuilder().ep({
32085
+ ...commonParams,
32086
+ Bucket: { type: "contextParams", name: "Bucket" },
32087
+ Prefix: { type: "contextParams", name: "Prefix" }
32088
+ }).m(function(Command2, cs, config2, o2) {
32089
+ return [
32090
+ getEndpointPlugin(config2, Command2.getEndpointParameterInstructions()),
32091
+ getThrow200ExceptionsPlugin(config2)
32092
+ ];
32093
+ }).s("AmazonS3", "ListObjectsV2", {}).n("S3Client", "ListObjectsV2Command").sc(ListObjectsV2$).build() {
32094
+ };
32095
+
32070
32096
  // ../../node_modules/.pnpm/@aws-sdk+client-s3@3.1048.0/node_modules/@aws-sdk/client-s3/dist-es/commands/PutObjectCommand.js
32071
32097
  var PutObjectCommand = class extends Command.classBuilder().ep({
32072
32098
  ...commonParams,
@@ -32113,6 +32139,34 @@ var StorageClient = class {
32113
32139
  );
32114
32140
  return formatPublicAssetUrl(this.bucket, this.region, key);
32115
32141
  }
32142
+ async deleteObject(key) {
32143
+ await this.client.send(
32144
+ new DeleteObjectCommand({ Bucket: this.bucket, Key: key })
32145
+ );
32146
+ }
32147
+ async listObjects(prefix) {
32148
+ const out = [];
32149
+ let token;
32150
+ do {
32151
+ const res = await this.client.send(
32152
+ new ListObjectsV2Command({
32153
+ Bucket: this.bucket,
32154
+ Prefix: prefix,
32155
+ ContinuationToken: token
32156
+ })
32157
+ );
32158
+ for (const obj of res.Contents ?? []) {
32159
+ if (!obj.Key) continue;
32160
+ out.push({
32161
+ key: obj.Key,
32162
+ size: obj.Size ?? 0,
32163
+ lastModified: obj.LastModified?.toISOString()
32164
+ });
32165
+ }
32166
+ token = res.IsTruncated ? res.NextContinuationToken : void 0;
32167
+ } while (token);
32168
+ return out;
32169
+ }
32116
32170
  };
32117
32171
 
32118
32172
  // src/server.ts
@@ -1,3 +1,5 @@
1
+ import { BundleExtractResult } from 'ampless';
2
+
1
3
  /**
2
4
  * Abstract contracts each tool handler depends on. Concrete
3
5
  * implementations live elsewhere:
@@ -15,6 +17,14 @@
15
17
  interface GraphqlClient {
16
18
  query<T>(operation: string, variables?: Record<string, unknown>): Promise<T>;
17
19
  }
20
+ interface StorageObject {
21
+ /** Full S3 key including any prefix. */
22
+ key: string;
23
+ /** Object size in bytes (0 when the backend can't supply it). */
24
+ size: number;
25
+ /** ISO 8601 timestamp of the last write, when the backend supplies it. */
26
+ lastModified?: string;
27
+ }
18
28
  interface StorageClient {
19
29
  /**
20
30
  * Upload `body` to the bucket at `key` with `contentType`. Returns
@@ -22,6 +32,18 @@ interface StorageClient {
22
32
  * https://{bucket}.s3.{region}.amazonaws.com/{key}).
23
33
  */
24
34
  putObject(key: string, body: Uint8Array, contentType: string): Promise<string>;
35
+ /**
36
+ * Remove the object at `key`. Implementations should treat a missing
37
+ * key as success (S3 DeleteObject is idempotent by default).
38
+ */
39
+ deleteObject(key: string): Promise<void>;
40
+ /**
41
+ * List every object under `prefix`. Implementations are expected to
42
+ * paginate internally so the caller gets the full set in a single
43
+ * resolved promise — the static-bundle tools never expect more than
44
+ * a few hundred entries per bundle in practice.
45
+ */
46
+ listObjects(prefix: string): Promise<StorageObject[]>;
25
47
  }
26
48
  interface ToolContext {
27
49
  graphql: GraphqlClient;
@@ -29,6 +51,51 @@ interface ToolContext {
29
51
  defaultSiteId: string;
30
52
  }
31
53
 
54
+ /**
55
+ * Server-side zip extractor used by the static-bundle MCP tools. Both
56
+ * the stdio CLI and the Lambda HTTP transport run this — `fflate` is
57
+ * a workspace dependency of `@ampless/mcp-server`, so the import
58
+ * resolves identically in both. The browser-side admin uploader has
59
+ * its own `extractZip(File)` based on JSZip (works on a `File`, not a
60
+ * `Buffer`).
61
+ *
62
+ * Behaviour parity with the admin extractor:
63
+ * - Skip directory entries (fflate gives byte arrays only, so this
64
+ * is naturally satisfied unless the zip has empty-byte directory
65
+ * placeholders — those still get filtered out by validateBundlePath
66
+ * returning "directory entry" for any path ending with `/`).
67
+ * - Silently drop OS-specific junk (`__MACOSX/*`, `.DS_Store`,
68
+ * `Thumbs.db`) so callers don't see them as bundle entries.
69
+ * - Surface structural issues (absolute paths, parent traversal, null
70
+ * bytes) so the tool can reject the upload.
71
+ * - Strip a common single top-level directory (macOS Finder zips
72
+ * wrap their contents in a folder).
73
+ */
74
+
75
+ interface ExtractZipOptions {
76
+ /**
77
+ * Hard ceiling on uncompressed bundle bytes. Defaults to 50 MB,
78
+ * matching the browser uploader's MAX_BUNDLE_BYTES. Callers running
79
+ * inside a tight Lambda envelope (Function URL payload cap ~6 MB
80
+ * post-base64) can tighten this; setting it lower than the bundle
81
+ * causes `extractZipFromBuffer` to throw before fully populating
82
+ * `files`.
83
+ */
84
+ maxBytes?: number;
85
+ }
86
+ /**
87
+ * Unzip `buffer` in memory and return the same shape the browser
88
+ * `extractZip(File)` helper returns. Throws if the unzip itself fails
89
+ * (corrupt archive) or if the uncompressed size exceeds `maxBytes`.
90
+ */
91
+ declare function extractZipFromBuffer(buffer: Uint8Array, opts?: ExtractZipOptions): BundleExtractResult;
92
+ /**
93
+ * Decode an extracted file's bytes as UTF-8 text. Used by the tool
94
+ * handler before running cross-file `validateBundle` so we don't
95
+ * instantiate a TextDecoder per entry. fflate provides a fast helper.
96
+ */
97
+ declare function decodeUtf8(data: Uint8Array): string;
98
+
32
99
  interface ToolDefinition {
33
100
  name: string;
34
101
  description: string;
@@ -49,4 +116,4 @@ declare function dispatchToolCall(name: string, args: Record<string, unknown>, c
49
116
  */
50
117
  declare function getTools(): readonly ToolDefinition[];
51
118
 
52
- export { type GraphqlClient, type StorageClient, type ToolContext, type ToolDefinition, dispatchToolCall, getTools, tools };
119
+ export { type ExtractZipOptions, type GraphqlClient, type StorageClient, type StorageObject, type ToolContext, type ToolDefinition, decodeUtf8, dispatchToolCall, extractZipFromBuffer, getTools, tools };
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ decodeUtf8,
3
4
  dispatchToolCall,
5
+ extractZipFromBuffer,
4
6
  getTools,
5
7
  tools
6
- } from "../chunk-EGQ7Q3Y3.js";
8
+ } from "../chunk-4PTNHZO4.js";
7
9
  import "../chunk-LMMQX4CK.js";
8
10
  export {
11
+ decodeUtf8,
9
12
  dispatchToolCall,
13
+ extractZipFromBuffer,
10
14
  getTools,
11
15
  tools
12
16
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/mcp-server",
3
- "version": "0.2.0-alpha.8",
3
+ "version": "0.2.0-alpha.9",
4
4
  "description": "MCP server for ampless — lets AI agents (Claude Desktop, Cursor, Claude Code) read and write posts and media via AppSync",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,8 @@
35
35
  "@modelcontextprotocol/sdk": "^1.0.0",
36
36
  "@aws-sdk/client-s3": "^3.1048.0",
37
37
  "amazon-cognito-identity-js": "^6.3.12",
38
- "ampless": "0.2.0-alpha.7"
38
+ "fflate": "^0.8.2",
39
+ "ampless": "0.2.0-alpha.8"
39
40
  },
40
41
  "keywords": [
41
42
  "ampless",