@ampless/backend 0.2.0-alpha.14 → 0.2.0-alpha.16

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * MCP HTTP endpoint Lambda. Phase 4: Bearer validation + JSON-RPC 2.0
3
- * tool dispatch over AppSync IAM auth.
2
+ * MCP HTTP endpoint Lambda. Phase 5: Bearer validation + JSON-RPC 2.0
3
+ * tool dispatch over AppSync IAM auth, including `upload_media`.
4
4
  *
5
5
  * 1. Reads KvStore directly (PK = 'mcp-tokens', SK = SHA-256 hex)
6
6
  * to validate `Authorization: Bearer amk_...`. Same narrow IAM
@@ -12,14 +12,14 @@
12
12
  * registry. The GraphqlClient implementation hits AppSync with
13
13
  * SigV4 (`AWS_IAM` auth mode), gated by `allow.resource(mcpHandler)
14
14
  * .to(['query', 'mutate'])` on Post / PostTag in the schema.
15
+ * 4. `upload_media` decodes the base64 body inline and uploads to S3
16
+ * using the Lambda execution role (s3:PutObject on public/media/*).
17
+ * Payload limit ~6 MB (base64-inflated) covers typical CMS images;
18
+ * large files should use the stdio CLI.
15
19
  *
16
20
  * Function URL event format: Lambda Function URLs emit API Gateway
17
21
  * HTTP v2 events (https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads).
18
22
  * Headers arrive lowercased.
19
- *
20
- * Note: `upload_media` is filtered out — the StorageClient flow needs
21
- * presigned S3 PUT URLs (the Lambda doesn't accept the binary body
22
- * itself), which lands in Phase 5.
23
23
  */
24
24
  interface FunctionUrlEvent {
25
25
  headers?: Record<string, string | undefined>;
@@ -4,10 +4,12 @@ import "../chunk-BYXBJQAS.js";
4
4
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
5
5
  import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
6
6
  import { createHash } from "crypto";
7
+ import { decodeAwsJson as decodeAwsJson2 } from "ampless";
7
8
 
8
- // ../mcp-server/dist/chunk-KM6F5QJH.js
9
- import { composeSiteIdStatus, composeSiteIdSlug } from "ampless";
10
- import { composeSiteIdStatus as composeSiteIdStatus2, composeSiteIdSlug as composeSiteIdSlug2 } from "ampless";
9
+ // ../mcp-server/dist/chunk-6LH275IC.js
10
+ import { decodeAwsJson } from "ampless";
11
+ import { composeSiteIdStatus, composeSiteIdSlug, encodeAwsJson } from "ampless";
12
+ import { composeSiteIdStatus as composeSiteIdStatus2, composeSiteIdSlug as composeSiteIdSlug2, encodeAwsJson as encodeAwsJson2 } from "ampless";
11
13
  var POST_FIELDS = (
12
14
  /* GraphQL */
13
15
  `
@@ -25,18 +27,6 @@ var POST_FIELDS = (
25
27
  }
26
28
  `
27
29
  );
28
- function decodeBody(value) {
29
- if (typeof value !== "string") return value;
30
- try {
31
- return JSON.parse(value);
32
- } catch {
33
- return value;
34
- }
35
- }
36
- function encodeBody(value) {
37
- if (typeof value === "string") return value;
38
- return JSON.stringify(value ?? null);
39
- }
40
30
  function toCorePost(p) {
41
31
  return {
42
32
  siteId: p.siteId,
@@ -45,7 +35,7 @@ function toCorePost(p) {
45
35
  title: p.title,
46
36
  excerpt: p.excerpt ?? void 0,
47
37
  format: p.format ?? "markdown",
48
- body: decodeBody(p.body),
38
+ body: decodeAwsJson(p.body),
49
39
  status: p.status ?? "draft",
50
40
  publishedAt: p.publishedAt ?? void 0,
51
41
  tags: (p.tags ?? []).filter((t) => typeof t === "string")
@@ -266,7 +256,7 @@ async function createPost(client, defaultSiteId, args) {
266
256
  title: args.title,
267
257
  excerpt: args.excerpt,
268
258
  format: args.format,
269
- body: encodeBody(args.body),
259
+ body: encodeAwsJson(args.body),
270
260
  status,
271
261
  publishedAt,
272
262
  tags: args.tags,
@@ -314,7 +304,7 @@ async function updatePost(client, defaultSiteId, args) {
314
304
  if (args.title !== void 0) input.title = args.title;
315
305
  if (args.excerpt !== void 0) input.excerpt = args.excerpt;
316
306
  if (args.format !== void 0) input.format = args.format;
317
- if (args.body !== void 0) input.body = encodeBody(args.body);
307
+ if (args.body !== void 0) input.body = encodeAwsJson2(args.body);
318
308
  if (args.status !== void 0) input.status = args.status;
319
309
  if (args.publishedAt !== void 0) input.publishedAt = args.publishedAt;
320
310
  if (args.tags !== void 0) input.tags = args.tags;
@@ -581,6 +571,26 @@ function createMcpGraphqlClient(opts) {
581
571
  };
582
572
  }
583
573
 
574
+ // src/functions/mcp-storage-client.ts
575
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
576
+ import { formatPublicAssetUrl } from "ampless";
577
+ function createMcpStorageClient(opts) {
578
+ const client = new S3Client({ region: opts.region });
579
+ return {
580
+ async putObject(key, body, contentType) {
581
+ await client.send(
582
+ new PutObjectCommand({
583
+ Bucket: opts.bucket,
584
+ Key: key,
585
+ Body: body,
586
+ ContentType: contentType
587
+ })
588
+ );
589
+ return formatPublicAssetUrl(opts.bucket, opts.region, key);
590
+ }
591
+ };
592
+ }
593
+
584
594
  // src/functions/mcp-handler.ts
585
595
  var TOKENS_PK = "mcp-tokens";
586
596
  var JSON_RPC_PARSE_ERROR = -32700;
@@ -596,9 +606,10 @@ function requireEnv(name) {
596
606
  }
597
607
  var KV_TABLE = requireEnv("AMPLESS_KV_TABLE");
598
608
  var APPSYNC_URL = requireEnv("AMPLESS_APPSYNC_URL");
609
+ var BUCKET_NAME = requireEnv("AMPLESS_BUCKET_NAME");
599
610
  var AWS_REGION = process.env["AWS_REGION"] ?? "us-east-1";
600
611
  var ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
601
- var HTTP_TOOLS = tools.filter((t) => t.name !== "upload_media");
612
+ var HTTP_TOOLS = tools;
602
613
  function hashToken(plain) {
603
614
  return createHash("sha256").update(plain).digest("hex");
604
615
  }
@@ -637,27 +648,23 @@ async function validateBearer(plaintext) {
637
648
  return meta;
638
649
  }
639
650
  function decodeTokenMeta(value) {
640
- if (typeof value === "object") return value;
641
- if (typeof value === "string") {
642
- try {
643
- return JSON.parse(value);
644
- } catch {
645
- return null;
646
- }
647
- }
648
- return null;
651
+ const parsed = decodeAwsJson2(value);
652
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
653
+ return parsed;
649
654
  }
650
655
  var cachedCtx = null;
651
656
  function makeContext(meta) {
652
657
  if (cachedCtx && cachedCtx.defaultSiteId === (meta.scope.siteId ?? "default")) {
653
658
  return cachedCtx;
654
659
  }
660
+ let storageClient = null;
655
661
  const ctx = {
656
662
  graphql: createMcpGraphqlClient({ endpoint: APPSYNC_URL, region: AWS_REGION }),
657
663
  storage: () => {
658
- throw new Error(
659
- "upload_media is not available on the HTTP MCP transport in v0.2 Phase 4 \u2014 use the stdio CLI or wait for Phase 5"
660
- );
664
+ if (!storageClient) {
665
+ storageClient = createMcpStorageClient({ bucket: BUCKET_NAME, region: AWS_REGION });
666
+ }
667
+ return storageClient;
661
668
  },
662
669
  defaultSiteId: meta.scope.siteId ?? "default"
663
670
  };
package/dist/index.js CHANGED
@@ -182,6 +182,14 @@ function defineAmplessBackend(opts) {
182
182
  "AMPLESS_APPSYNC_URL",
183
183
  backend.data.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl
184
184
  );
185
+ mcpHandlerFn.addToRolePolicy(
186
+ new PolicyStatement({
187
+ effect: Effect.ALLOW,
188
+ actions: ["s3:PutObject"],
189
+ resources: [`${backend.storage.resources.bucket.bucketArn}/public/media/*`]
190
+ })
191
+ );
192
+ mcpHandlerFn.addEnvironment("AMPLESS_BUCKET_NAME", backend.storage.resources.bucket.bucketName);
185
193
  const mcpFunctionUrl = mcpHandlerFn.addFunctionUrl({
186
194
  authType: FunctionUrlAuthType.NONE,
187
195
  cors: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/backend",
3
- "version": "0.2.0-alpha.14",
3
+ "version": "0.2.0-alpha.16",
4
4
  "description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,8 +64,8 @@
64
64
  "@aws-sdk/util-dynamodb": "^3.717.0",
65
65
  "@smithy/protocol-http": "^5.3.0",
66
66
  "@smithy/signature-v4": "^5.4.0",
67
- "ampless": "0.2.0-alpha.6",
68
- "@ampless/mcp-server": "0.2.0-alpha.6"
67
+ "ampless": "0.2.0-alpha.7",
68
+ "@ampless/mcp-server": "0.2.0-alpha.7"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@aws-amplify/backend": "^1",