@ampless/backend 1.0.0-beta.87 → 1.0.0-beta.89

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.
@@ -3,7 +3,7 @@ import { ConditionalCheckFailedException, DynamoDBClient } from "@aws-sdk/client
3
3
  import { DynamoDBDocumentClient, GetCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
4
4
  import { createHash } from "crypto";
5
5
 
6
- // ../mcp-server/dist/index.js
6
+ // ../mcp-server/dist/tools/index.js
7
7
  import { filterSortPostSummaries } from "ampless";
8
8
  import {
9
9
  decodeAwsJson
@@ -1683,50 +1683,66 @@ var tools = [
1683
1683
  name: "list_posts",
1684
1684
  description: "Returns lightweight post summaries (no body \u2014 use get_post for content) with search / sort / filters. `total` reflects the filtered count. Supports query (substring over title/slug/tags), tag (exact), status, sort, limit (1\u2013100, default 20), offset (default 0).",
1685
1685
  inputSchema: listPostsSchema,
1686
- handler: (args, ctx) => listPosts(ctx.graphql, args)
1686
+ handler: (args, ctx) => listPosts(ctx.graphql, args),
1687
+ readOnly: true,
1688
+ destructive: false
1687
1689
  },
1688
1690
  {
1689
1691
  name: "get_post",
1690
1692
  description: "Fetch a single post by slug or postId. Returns null if not found.",
1691
1693
  inputSchema: getPostSchema,
1692
- handler: (args, ctx) => getPost(ctx.graphql, args)
1694
+ handler: (args, ctx) => getPost(ctx.graphql, args),
1695
+ readOnly: true,
1696
+ destructive: false
1693
1697
  },
1694
1698
  {
1695
1699
  name: "create_post",
1696
1700
  description: 'Create a new post. Title and slug are required. Body shape depends on format: tiptap=JSON node tree, markdown=source string, html=raw HTML string. Defaults to status=draft. Pass `metadata: { no_layout: true }` alongside format=html to publish the body as a bare HTML page with no theme chrome (middleware rewrites the /<slug> request to the internal bare-HTML handler). Pass `metadata: { cache: "deep" | "hot" }` to override the default cooldown-based cache strategy \u2014 see get_schema.notes.cacheStrategy for details.',
1697
1701
  inputSchema: createPostSchema,
1698
- handler: (args, ctx) => createPost(ctx.graphql, args)
1702
+ handler: (args, ctx) => createPost(ctx.graphql, args),
1703
+ readOnly: false,
1704
+ destructive: false
1699
1705
  },
1700
1706
  {
1701
1707
  name: "update_post",
1702
1708
  description: "Update an existing post by postId. Only the fields you pass are changed. Tag list / publishedAt changes also update the PostTag denormalized index. Passing `metadata` REPLACES the existing object \u2014 call get_post first if you only want to add or change one key.",
1703
1709
  inputSchema: updatePostSchema,
1704
- handler: (args, ctx) => updatePost(ctx.graphql, args)
1710
+ handler: (args, ctx) => updatePost(ctx.graphql, args),
1711
+ readOnly: false,
1712
+ // Overwrites an existing post's fields — not a pure additive write.
1713
+ destructive: true
1705
1714
  },
1706
1715
  {
1707
1716
  name: "delete_post",
1708
1717
  description: "Delete a post by postId. Also drops associated PostTag index entries.",
1709
1718
  inputSchema: deletePostSchema,
1710
1719
  handler: (args, ctx) => deletePost(ctx.graphql, args),
1720
+ readOnly: false,
1711
1721
  destructive: true
1712
1722
  },
1713
1723
  {
1714
1724
  name: "upload_media",
1715
1725
  description: "Upload a file to the site's media S3 bucket. Pass base64-encoded bytes; the server stores them verbatim under public/media/YYYY/MM/. Returns the public URL and Media record. The server does not transcode \u2014 pre-process (e.g. resize/webp) on the client.",
1716
1726
  inputSchema: uploadMediaSchema,
1717
- handler: (args, ctx) => uploadMedia(ctx.graphql, ctx.storage(), args)
1727
+ handler: (args, ctx) => uploadMedia(ctx.graphql, ctx.storage(), args),
1728
+ readOnly: false,
1729
+ destructive: false
1718
1730
  },
1719
1731
  {
1720
1732
  name: "list_media",
1721
1733
  description: 'List media files in the CMS. Optional filters: `mimeType` (prefix match \u2014 "image/" matches all images, "image/png" only PNG), `prefix` (S3 key prefix on `src`, e.g. "public/media/2024/"), `createdAfter` / `createdBefore` (ISO 8601 date range). Returns up to `limit` rows (default 20) \u2014 each `{ mediaId, src, url, mimeType, size, createdAt, updatedAt }` \u2014 plus a `nextToken` cursor. Note: filters apply after the page read, so a page may return fewer than `limit` rows while a `nextToken` remains \u2014 follow the cursor. Use this to find media to delete without remembering `upload_media` responses.',
1722
1734
  inputSchema: listMediaSchema,
1723
- handler: (args, ctx) => listMedia(ctx.graphql, ctx.storage(), args)
1735
+ handler: (args, ctx) => listMedia(ctx.graphql, ctx.storage(), args),
1736
+ readOnly: true,
1737
+ destructive: false
1724
1738
  },
1725
1739
  {
1726
1740
  name: "search_media",
1727
1741
  description: 'Search media by substring (case-sensitive `contains`) across `src` (which includes the filename) and `mimeType`. Pass `query` (e.g. "logo", ".png", "image/png"). Walks DynamoDB pages internally until it collects at least `limit` matches (default 20), exhausts the table, or hits its page cap \u2014 so `limit` is a soft target and the result may run slightly past it. Returns the same row shape as `list_media` plus `nextToken`; `truncated: true` means the page cap was hit with more to scan (pass `nextToken` back to continue).',
1728
1742
  inputSchema: searchMediaSchema,
1729
- handler: (args, ctx) => searchMedia(ctx.graphql, ctx.storage(), args)
1743
+ handler: (args, ctx) => searchMedia(ctx.graphql, ctx.storage(), args),
1744
+ readOnly: true,
1745
+ destructive: false
1730
1746
  },
1731
1747
  {
1732
1748
  name: "delete_media",
@@ -1737,13 +1753,16 @@ var tools = [
1737
1753
  ctx.storage(),
1738
1754
  args
1739
1755
  ),
1756
+ readOnly: false,
1740
1757
  destructive: true
1741
1758
  },
1742
1759
  {
1743
1760
  name: "get_schema",
1744
1761
  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.",
1745
1762
  inputSchema: getSchemaSchema,
1746
- handler: async () => getSchema()
1763
+ handler: async () => getSchema(),
1764
+ readOnly: true,
1765
+ destructive: false
1747
1766
  },
1748
1767
  {
1749
1768
  name: "upload_static_bundle",
@@ -1754,6 +1773,7 @@ var tools = [
1754
1773
  ctx.storage(),
1755
1774
  args
1756
1775
  ),
1776
+ readOnly: false,
1757
1777
  destructive: true
1758
1778
  },
1759
1779
  {
@@ -1763,7 +1783,10 @@ var tools = [
1763
1783
  handler: (args, ctx) => uploadStaticFile(
1764
1784
  ctx.storage(),
1765
1785
  args
1766
- )
1786
+ ),
1787
+ readOnly: false,
1788
+ // Overwrites whatever file currently sits at that S3 key.
1789
+ destructive: true
1767
1790
  },
1768
1791
  {
1769
1792
  name: "delete_static_file",
@@ -1773,6 +1796,7 @@ var tools = [
1773
1796
  ctx.storage(),
1774
1797
  args
1775
1798
  ),
1799
+ readOnly: false,
1776
1800
  destructive: true
1777
1801
  },
1778
1802
  {
@@ -1783,13 +1807,128 @@ var tools = [
1783
1807
  ctx.graphql,
1784
1808
  ctx.storage(),
1785
1809
  args
1786
- )
1810
+ ),
1811
+ readOnly: false,
1812
+ // Rebuilds (overwrites) the existing Post row's manifest.
1813
+ destructive: true
1787
1814
  }
1788
1815
  ];
1789
- async function dispatchToolCall(name, args, ctx) {
1790
- const tool = tools.find((t) => t.name === name);
1791
- if (!tool) return null;
1792
- return tool.handler(args, ctx);
1816
+
1817
+ // ../mcp-server/dist/jsonrpc/index.js
1818
+ var JSON_RPC_PARSE_ERROR = -32700;
1819
+ var JSON_RPC_INVALID_REQUEST = -32600;
1820
+ var JSON_RPC_METHOD_NOT_FOUND = -32601;
1821
+ var JSON_RPC_INVALID_PARAMS = -32602;
1822
+ var JSON_RPC_INTERNAL_ERROR = -32603;
1823
+ var SUPPORTED_PROTOCOL_VERSIONS = ["2025-03-26", "2024-11-05"];
1824
+ var LATEST_SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
1825
+ function jsonRpcResult(id, result) {
1826
+ return { jsonrpc: "2.0", id, result };
1827
+ }
1828
+ function jsonRpcError(id, code, message, data) {
1829
+ const error = { code, message };
1830
+ if (data !== void 0) error.data = data;
1831
+ return { jsonrpc: "2.0", id, error };
1832
+ }
1833
+ function isNotification(req) {
1834
+ return req.id === void 0;
1835
+ }
1836
+ function hasValidJsonRpcId(req) {
1837
+ const id = req.id;
1838
+ if (id === void 0) return true;
1839
+ return typeof id === "string" || typeof id === "number" && Number.isInteger(id);
1840
+ }
1841
+ function isPlainObject(v) {
1842
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1843
+ }
1844
+ function toolAnnotations(t) {
1845
+ const annotations = {};
1846
+ if (typeof t.readOnly === "boolean") annotations.readOnlyHint = t.readOnly;
1847
+ if (typeof t.destructive === "boolean") annotations.destructiveHint = t.destructive;
1848
+ return annotations;
1849
+ }
1850
+ async function dispatchJsonRpc(req, opts) {
1851
+ if (!hasValidJsonRpcId(req)) {
1852
+ return jsonRpcError(
1853
+ null,
1854
+ JSON_RPC_INVALID_REQUEST,
1855
+ "Invalid Request: `id` must be a string or an integer"
1856
+ );
1857
+ }
1858
+ const response = await dispatchMethod(req, opts);
1859
+ return isNotification(req) ? null : response;
1860
+ }
1861
+ async function dispatchMethod(req, opts) {
1862
+ const id = req.id ?? null;
1863
+ switch (req.method) {
1864
+ case "initialize": {
1865
+ const requested = req.params?.protocolVersion;
1866
+ if (typeof requested !== "string") {
1867
+ return jsonRpcError(
1868
+ id,
1869
+ JSON_RPC_INVALID_PARAMS,
1870
+ "initialize requires a string `protocolVersion` parameter"
1871
+ );
1872
+ }
1873
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(
1874
+ requested
1875
+ ) ? requested : LATEST_SUPPORTED_PROTOCOL_VERSION;
1876
+ return jsonRpcResult(id, {
1877
+ protocolVersion,
1878
+ capabilities: { tools: {} },
1879
+ serverInfo: opts.serverInfo
1880
+ });
1881
+ }
1882
+ case "notifications/initialized":
1883
+ return jsonRpcResult(id, {});
1884
+ case "tools/list":
1885
+ return jsonRpcResult(id, {
1886
+ tools: opts.tools.map((t) => ({
1887
+ name: t.name,
1888
+ description: t.description,
1889
+ inputSchema: t.inputSchema,
1890
+ annotations: toolAnnotations(t)
1891
+ }))
1892
+ });
1893
+ case "tools/call": {
1894
+ const params = req.params;
1895
+ if (!params || typeof params.name !== "string") {
1896
+ return jsonRpcError(id, JSON_RPC_INVALID_PARAMS, "tools/call requires a `name` parameter");
1897
+ }
1898
+ const rawArgs = params.arguments;
1899
+ if (rawArgs !== void 0 && !isPlainObject(rawArgs)) {
1900
+ return jsonRpcError(
1901
+ id,
1902
+ JSON_RPC_INVALID_PARAMS,
1903
+ "tools/call `arguments` must be an object"
1904
+ );
1905
+ }
1906
+ const tool = opts.tools.find((t) => t.name === params.name);
1907
+ if (!tool) {
1908
+ return jsonRpcError(id, JSON_RPC_METHOD_NOT_FOUND, `unknown tool: ${params.name}`);
1909
+ }
1910
+ const ctx = opts.getContext();
1911
+ try {
1912
+ const result = await tool.handler(rawArgs ?? {}, ctx);
1913
+ return jsonRpcResult(id, {
1914
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1915
+ });
1916
+ } catch (err2) {
1917
+ const rawMessage = err2 instanceof Error ? err2.message : String(err2);
1918
+ console.error("[mcp-jsonrpc] tool dispatch failed", {
1919
+ tool: params.name,
1920
+ message: rawMessage
1921
+ });
1922
+ const message = opts.formatToolError ? opts.formatToolError(err2) : rawMessage;
1923
+ return jsonRpcResult(id, {
1924
+ isError: true,
1925
+ content: [{ type: "text", text: message }]
1926
+ });
1927
+ }
1928
+ }
1929
+ default:
1930
+ return jsonRpcError(id, JSON_RPC_METHOD_NOT_FOUND, `Method not found: ${req.method}`);
1931
+ }
1793
1932
  }
1794
1933
 
1795
1934
  // src/functions/mcp-graphql-client.ts
@@ -1909,12 +2048,6 @@ function createMcpStorageClient(opts) {
1909
2048
  }
1910
2049
 
1911
2050
  // src/functions/mcp-handler.ts
1912
- var JSON_RPC_PARSE_ERROR = -32700;
1913
- var JSON_RPC_INVALID_REQUEST = -32600;
1914
- var JSON_RPC_METHOD_NOT_FOUND = -32601;
1915
- var JSON_RPC_INVALID_PARAMS = -32602;
1916
- var JSON_RPC_INTERNAL_ERROR = -32603;
1917
- var MCP_PROTOCOL_VERSION = "2024-11-05";
1918
2051
  var LAST_USED_THROTTLE_MS = 6e4;
1919
2052
  function requireEnv(name) {
1920
2053
  const v = process.env[name];
@@ -1937,14 +2070,6 @@ function jsonResponse(statusCode, body) {
1937
2070
  body: JSON.stringify(body)
1938
2071
  };
1939
2072
  }
1940
- function jsonRpcResult(id, result) {
1941
- return { jsonrpc: "2.0", id, result };
1942
- }
1943
- function jsonRpcError(id, code, message, data) {
1944
- const error = { code, message };
1945
- if (data !== void 0) error.data = data;
1946
- return { jsonrpc: "2.0", id, error };
1947
- }
1948
2073
  async function validateBearer(plaintext) {
1949
2074
  const hash = hashToken(plaintext);
1950
2075
  const res = await ddb.send(
@@ -1997,55 +2122,6 @@ function makeContext() {
1997
2122
  cachedCtx = ctx;
1998
2123
  return ctx;
1999
2124
  }
2000
- async function dispatchJsonRpc(req) {
2001
- switch (req.method) {
2002
- case "initialize":
2003
- return jsonRpcResult(req.id, {
2004
- protocolVersion: MCP_PROTOCOL_VERSION,
2005
- capabilities: { tools: {} },
2006
- serverInfo: { name: "ampless-mcp", version: "0.2" }
2007
- });
2008
- case "notifications/initialized":
2009
- return jsonRpcResult(req.id, null);
2010
- case "tools/list":
2011
- return jsonRpcResult(req.id, {
2012
- tools: HTTP_TOOLS.map((t) => ({
2013
- name: t.name,
2014
- description: t.description,
2015
- inputSchema: t.inputSchema
2016
- }))
2017
- });
2018
- case "tools/call": {
2019
- const params = req.params;
2020
- if (!params?.name || typeof params.name !== "string") {
2021
- return jsonRpcError(req.id, JSON_RPC_INVALID_PARAMS, "tools/call requires a `name` parameter");
2022
- }
2023
- const tool = HTTP_TOOLS.find((t) => t.name === params.name);
2024
- if (!tool) {
2025
- return jsonRpcError(req.id, JSON_RPC_METHOD_NOT_FOUND, `unknown tool: ${params.name}`);
2026
- }
2027
- const ctx = makeContext();
2028
- try {
2029
- const result = await dispatchToolCall(params.name, params.arguments ?? {}, ctx);
2030
- return jsonRpcResult(req.id, {
2031
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2032
- });
2033
- } catch (err2) {
2034
- const message = err2 instanceof Error ? err2.message : String(err2);
2035
- console.error("[mcp-handler] tool dispatch failed", {
2036
- tool: params.name,
2037
- message
2038
- });
2039
- return jsonRpcResult(req.id, {
2040
- isError: true,
2041
- content: [{ type: "text", text: message }]
2042
- });
2043
- }
2044
- }
2045
- default:
2046
- return jsonRpcError(req.id, JSON_RPC_METHOD_NOT_FOUND, `Method not found: ${req.method}`);
2047
- }
2048
- }
2049
2125
  var handler = async (event) => {
2050
2126
  if (event.requestContext?.http?.method === "OPTIONS") {
2051
2127
  return { statusCode: 204, body: "" };
@@ -2074,14 +2150,25 @@ var handler = async (event) => {
2074
2150
  } catch {
2075
2151
  return jsonResponse(400, jsonRpcError(null, JSON_RPC_PARSE_ERROR, "Parse error"));
2076
2152
  }
2077
- if (req.jsonrpc !== "2.0" || typeof req.method !== "string") {
2153
+ if (typeof req !== "object" || req === null || Array.isArray(req)) {
2154
+ return jsonResponse(400, jsonRpcError(null, JSON_RPC_INVALID_REQUEST, "Invalid Request"));
2155
+ }
2156
+ if (req.jsonrpc !== "2.0" || typeof req.method !== "string" || !hasValidJsonRpcId(req)) {
2157
+ const responseId = hasValidJsonRpcId(req) ? req.id ?? null : null;
2078
2158
  return jsonResponse(
2079
2159
  400,
2080
- jsonRpcError(req?.id ?? null, JSON_RPC_INVALID_REQUEST, "Invalid Request")
2160
+ jsonRpcError(responseId, JSON_RPC_INVALID_REQUEST, "Invalid Request")
2081
2161
  );
2082
2162
  }
2083
2163
  try {
2084
- const response = await dispatchJsonRpc(req);
2164
+ const response = await dispatchJsonRpc(req, {
2165
+ tools: HTTP_TOOLS,
2166
+ getContext: makeContext,
2167
+ serverInfo: { name: "ampless-mcp", version: "0.2" }
2168
+ });
2169
+ if (response === null) {
2170
+ return { statusCode: 202, body: "" };
2171
+ }
2085
2172
  return jsonResponse(200, response);
2086
2173
  } catch (err2) {
2087
2174
  const message = err2 instanceof Error ? err2.message : String(err2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/backend",
3
- "version": "1.0.0-beta.87",
3
+ "version": "1.0.0-beta.89",
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",
@@ -69,8 +69,8 @@
69
69
  "@smithy/protocol-http": "^5.4.4",
70
70
  "@smithy/signature-v4": "^5.4.4",
71
71
  "fflate": "^0.8.3",
72
- "@ampless/mcp-server": "1.0.0-beta.64",
73
- "ampless": "1.0.0-beta.57"
72
+ "@ampless/mcp-server": "1.0.0-beta.66",
73
+ "ampless": "1.0.0-beta.59"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@aws-amplify/backend": "^1.19.0",