diff --git a/.changeset/complete-mcp-release.md b/.changeset/complete-mcp-release.md new file mode 100644 index 0000000..2a5513e --- /dev/null +++ b/.changeset/complete-mcp-release.md @@ -0,0 +1,9 @@ +--- +'@paymesh/mcp': minor +'paymesh': minor +'@paymesh/postgres': minor +'@paymesh/drizzle': minor +'@paymesh/prisma': minor +--- + +Add the official Paymesh MCP package with client/provider introspection tools, database-backed customer lookup by email and external id, and the corresponding docs page for installation and usage. diff --git a/.gitignore b/.gitignore index 771597c..12ebc1c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store -client.ts +/client.ts diff --git a/apps/web/.source/browser.ts b/apps/web/.source/browser.ts index a2e4232..7cf14f5 100644 --- a/apps/web/.source/browser.ts +++ b/apps/web/.source/browser.ts @@ -7,6 +7,6 @@ const create = browser(); const browserCollections = { - docs: create.doc("docs", {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), }), + docs: create.doc("docs", {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), }), }; export default browserCollections; \ No newline at end of file diff --git a/apps/web/.source/server.ts b/apps/web/.source/server.ts index 11b0bd5..481ba5c 100644 --- a/apps/web/.source/server.ts +++ b/apps/web/.source/server.ts @@ -1,43 +1,44 @@ // @ts-nocheck -import { frontmatter as __fd_glob_42 } from "../content/docs/reference/plugin-api.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_41 } from "../content/docs/reference/errors.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_40 } from "../content/docs/reference/database-schema.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_39 } from "../content/docs/reference/client-options.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_38 } from "../content/docs/reference/cli.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_37 } from "../content/docs/providers/stripe.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_36 } from "../content/docs/providers/polar.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_35 } from "../content/docs/providers/abacatepay.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_34 } from "../content/docs/database/prisma.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_33 } from "../content/docs/database/postgres.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_32 } from "../content/docs/database/overview.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_31 } from "../content/docs/database/drizzle.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_30 } from "../content/docs/plugins/overview.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_29 } from "../content/docs/plugins/dash.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_28 } from "../content/docs/plugins/audit-logs.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_27 } from "../content/docs/guides/webhooks.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_26 } from "../content/docs/guides/webhooks-at-scale.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_25 } from "../content/docs/guides/testing.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_24 } from "../content/docs/guides/provider-selection.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_23 } from "../content/docs/guides/plugins.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_22 } from "../content/docs/guides/optimize-performance.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_21 } from "../content/docs/guides/database.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_20 } from "../content/docs/guides/create-your-first-plugin.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_19 } from "../content/docs/guides/create-a-provider.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_18 } from "../content/docs/guides/create-a-database-adapter.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_17 } from "../content/docs/concepts/typescript.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_16 } from "../content/docs/concepts/sandbox.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_15 } from "../content/docs/concepts/pix.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_14 } from "../content/docs/concepts/payment-providers.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_13 } from "../content/docs/concepts/hooks.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_12 } from "../content/docs/concepts/customers.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_11 } from "../content/docs/concepts/client.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_10 } from "../content/docs/concepts/api.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_9 } from "../content/docs/ai-resources/skills.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_8 } from "../content/docs/adapters/next.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_7 } from "../content/docs/adapters/hono.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_6 } from "../content/docs/adapters/fastify.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_5 } from "../content/docs/adapters/express.mdx?collection=docs&only=frontmatter" -import { frontmatter as __fd_glob_4 } from "../content/docs/adapters/elysia.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_43 } from "../content/docs/reference/plugin-api.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_42 } from "../content/docs/reference/errors.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_41 } from "../content/docs/reference/database-schema.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_40 } from "../content/docs/reference/client-options.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_39 } from "../content/docs/reference/cli.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_38 } from "../content/docs/providers/stripe.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_37 } from "../content/docs/providers/polar.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_36 } from "../content/docs/providers/abacatepay.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_35 } from "../content/docs/plugins/overview.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_34 } from "../content/docs/plugins/dash.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_33 } from "../content/docs/plugins/audit-logs.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_32 } from "../content/docs/guides/webhooks.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_31 } from "../content/docs/guides/webhooks-at-scale.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_30 } from "../content/docs/guides/testing.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_29 } from "../content/docs/guides/provider-selection.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_28 } from "../content/docs/guides/plugins.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_27 } from "../content/docs/guides/optimize-performance.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_26 } from "../content/docs/guides/database.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_25 } from "../content/docs/guides/create-your-first-plugin.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_24 } from "../content/docs/guides/create-a-provider.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_23 } from "../content/docs/guides/create-a-database-adapter.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_22 } from "../content/docs/concepts/typescript.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_21 } from "../content/docs/concepts/sandbox.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_20 } from "../content/docs/concepts/pix.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_19 } from "../content/docs/concepts/payment-providers.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_18 } from "../content/docs/concepts/hooks.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_17 } from "../content/docs/concepts/customers.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_16 } from "../content/docs/concepts/client.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_15 } from "../content/docs/concepts/api.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_14 } from "../content/docs/database/prisma.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_13 } from "../content/docs/database/postgres.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_12 } from "../content/docs/database/overview.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_11 } from "../content/docs/database/drizzle.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_10 } from "../content/docs/ai-resources/skills.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_9 } from "../content/docs/adapters/next.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_8 } from "../content/docs/adapters/hono.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_7 } from "../content/docs/adapters/fastify.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_6 } from "../content/docs/adapters/express.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_5 } from "../content/docs/adapters/elysia.mdx?collection=docs&only=frontmatter" +import { frontmatter as __fd_glob_4 } from "../content/docs/mcp.mdx?collection=docs&only=frontmatter" import { frontmatter as __fd_glob_3 } from "../content/docs/introduction.mdx?collection=docs&only=frontmatter" import { frontmatter as __fd_glob_2 } from "../content/docs/installation.mdx?collection=docs&only=frontmatter" import { frontmatter as __fd_glob_1 } from "../content/docs/comparison.mdx?collection=docs&only=frontmatter" @@ -50,4 +51,4 @@ const create = server({"doc":{"passthroughs":["extractedReferences"]}}); -export const docs = await create.docsLazy("docs", "content/docs", {}, {"basic-usage.mdx": __fd_glob_0, "comparison.mdx": __fd_glob_1, "installation.mdx": __fd_glob_2, "introduction.mdx": __fd_glob_3, "adapters/elysia.mdx": __fd_glob_4, "adapters/express.mdx": __fd_glob_5, "adapters/fastify.mdx": __fd_glob_6, "adapters/hono.mdx": __fd_glob_7, "adapters/next.mdx": __fd_glob_8, "ai-resources/skills.mdx": __fd_glob_9, "concepts/api.mdx": __fd_glob_10, "concepts/client.mdx": __fd_glob_11, "concepts/customers.mdx": __fd_glob_12, "concepts/hooks.mdx": __fd_glob_13, "concepts/payment-providers.mdx": __fd_glob_14, "concepts/pix.mdx": __fd_glob_15, "concepts/sandbox.mdx": __fd_glob_16, "concepts/typescript.mdx": __fd_glob_17, "guides/create-a-database-adapter.mdx": __fd_glob_18, "guides/create-a-provider.mdx": __fd_glob_19, "guides/create-your-first-plugin.mdx": __fd_glob_20, "guides/database.mdx": __fd_glob_21, "guides/optimize-performance.mdx": __fd_glob_22, "guides/plugins.mdx": __fd_glob_23, "guides/provider-selection.mdx": __fd_glob_24, "guides/testing.mdx": __fd_glob_25, "guides/webhooks-at-scale.mdx": __fd_glob_26, "guides/webhooks.mdx": __fd_glob_27, "plugins/audit-logs.mdx": __fd_glob_28, "plugins/dash.mdx": __fd_glob_29, "plugins/overview.mdx": __fd_glob_30, "database/drizzle.mdx": __fd_glob_31, "database/overview.mdx": __fd_glob_32, "database/postgres.mdx": __fd_glob_33, "database/prisma.mdx": __fd_glob_34, "providers/abacatepay.mdx": __fd_glob_35, "providers/polar.mdx": __fd_glob_36, "providers/stripe.mdx": __fd_glob_37, "reference/cli.mdx": __fd_glob_38, "reference/client-options.mdx": __fd_glob_39, "reference/database-schema.mdx": __fd_glob_40, "reference/errors.mdx": __fd_glob_41, "reference/plugin-api.mdx": __fd_glob_42, }, {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), }); \ No newline at end of file +export const docs = await create.docsLazy("docs", "content/docs", {}, {"basic-usage.mdx": __fd_glob_0, "comparison.mdx": __fd_glob_1, "installation.mdx": __fd_glob_2, "introduction.mdx": __fd_glob_3, "mcp.mdx": __fd_glob_4, "adapters/elysia.mdx": __fd_glob_5, "adapters/express.mdx": __fd_glob_6, "adapters/fastify.mdx": __fd_glob_7, "adapters/hono.mdx": __fd_glob_8, "adapters/next.mdx": __fd_glob_9, "ai-resources/skills.mdx": __fd_glob_10, "database/drizzle.mdx": __fd_glob_11, "database/overview.mdx": __fd_glob_12, "database/postgres.mdx": __fd_glob_13, "database/prisma.mdx": __fd_glob_14, "concepts/api.mdx": __fd_glob_15, "concepts/client.mdx": __fd_glob_16, "concepts/customers.mdx": __fd_glob_17, "concepts/hooks.mdx": __fd_glob_18, "concepts/payment-providers.mdx": __fd_glob_19, "concepts/pix.mdx": __fd_glob_20, "concepts/sandbox.mdx": __fd_glob_21, "concepts/typescript.mdx": __fd_glob_22, "guides/create-a-database-adapter.mdx": __fd_glob_23, "guides/create-a-provider.mdx": __fd_glob_24, "guides/create-your-first-plugin.mdx": __fd_glob_25, "guides/database.mdx": __fd_glob_26, "guides/optimize-performance.mdx": __fd_glob_27, "guides/plugins.mdx": __fd_glob_28, "guides/provider-selection.mdx": __fd_glob_29, "guides/testing.mdx": __fd_glob_30, "guides/webhooks-at-scale.mdx": __fd_glob_31, "guides/webhooks.mdx": __fd_glob_32, "plugins/audit-logs.mdx": __fd_glob_33, "plugins/dash.mdx": __fd_glob_34, "plugins/overview.mdx": __fd_glob_35, "providers/abacatepay.mdx": __fd_glob_36, "providers/polar.mdx": __fd_glob_37, "providers/stripe.mdx": __fd_glob_38, "reference/cli.mdx": __fd_glob_39, "reference/client-options.mdx": __fd_glob_40, "reference/database-schema.mdx": __fd_glob_41, "reference/errors.mdx": __fd_glob_42, "reference/plugin-api.mdx": __fd_glob_43, }, {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), }); \ No newline at end of file diff --git a/apps/web/content/docs/mcp.mdx b/apps/web/content/docs/mcp.mdx new file mode 100644 index 0000000..be0571a --- /dev/null +++ b/apps/web/content/docs/mcp.mdx @@ -0,0 +1,144 @@ +--- +title: MCP +description: Turn any Paymesh client into an MCP server with the same runtime config your app already uses. +--- + +## Paymesh, as a local AI server + +`@paymesh/mcp` exposes your real Paymesh client as an MCP server over stdio. + +That means you can ask an AI tool to work against the same billing layer your app already uses, without duplicating provider credentials, schemas, or operational rules. + +## Install + +```bash +npm install paymesh @paymesh/mcp +``` + +If you already have a local Paymesh client file, the MCP package can import it directly. + +## Start it + +```bash +npx @paymesh/mcp \ + --client ./client.ts \ + --export paymesh \ + --readonly +``` + +If your client uses a live provider by default, add `--allow-live-mode` or configure sandbox mode in the client itself. + +```bash +npx @paymesh/mcp \ + --client ./client.ts \ + --export paymesh \ + --readonly \ + --allow-live-mode +``` + +## Configure it from the CLI + +```bash +paymesh mcp \ + --client ./client.ts \ + --export paymesh \ + --codex +``` + +The `paymesh mcp` command can also configure Cursor, Claude Code, and Open Code. When you run it without a target flag, it opens an interactive prompt and asks which client you want to configure. + +## Use it with Codex + +```bash +paymesh mcp \ + --client ./src/lib/paymesh.ts \ + --export billing \ + --codex +``` + +Codex stores MCP settings in `~/.codex/config.toml` by default, or `.codex/config.toml` in trusted projects. `paymesh mcp --codex` configures the Paymesh MCP server using the same `npx @paymesh/mcp ...` command that the Codex CLI expects. + +## CLI options + +The CLI is intentionally small and maps directly to the client metadata plus a few overrides. + +| Option | Meaning | +| --- | --- | +| `--client ` | Path to the Paymesh client module. | +| `--export ` | Named export to load from the module. | +| `--cursor` | Configure Cursor automatically. | +| `--claude-code` | Configure Claude Code automatically. | +| `--codex` | Configure Codex automatically. | +| `--open-code` | Configure Open Code automatically. | +| `--manual` | Print the generated MCP config instead of applying it. | +| `--enabled` / `--no-enabled` | Turn the MCP server on or off. | +| `--readonly` / `--no-readonly` | Expose only read-only tools or allow writes. | +| `--include-raw` / `--no-include-raw` | Include raw provider payloads in tool output. | +| `--allow-live-mode` / `--no-allow-live-mode` | Permit live-mode provider usage. | +| `--max-list-limit ` | Limit list tools to a maximum page size. | +| `--max-list-lines ` | Alias for `--max-list-limit`. | +| `--customers` / `--no-customers` | Enable or disable customer tools. | +| `--payments` / `--no-payments` | Enable or disable payment tools. | +| `--pix` / `--no-pix` | Enable or disable PIX tools. | +| `--plugins` / `--no-plugins` | Enable or disable plugin introspection tools. | + +## Tools + +The server currently exposes these tools: + +| Tool | What it does | +| --- | --- | +| `capabilities_list` | Lists the enabled capabilities on the active Paymesh client. | +| `provider_info` | Shows the active provider, sandbox/live mode, and provider capabilities. | +| `customers_list` | Lists customers from the configured database. | +| `customers_get` | Loads a customer by provider id. | +| `customers_get_by_email` | Loads a customer by email from the configured database. | +| `customers_get_by_external_id` | Loads a customer by external id from the configured database. | +| `customers_upsert` | Creates or updates a customer. | +| `customers_delete` | Deletes a customer by id. | +| `payments_create` | Creates a payment. | +| `pix_create` | Creates a PIX payment. | +| `pix_get` | Loads a PIX payment by id. | +| `plugins_list` | Lists registered Paymesh plugins. | + +## Configuration precedence + +The final MCP config is resolved in this order: + +1. Built-in defaults. +2. `client.$mcp` from your Paymesh client. +3. Flags passed to the MCP CLI. + +That keeps the MCP server aligned with the same runtime client your app already trusts. + +## Example client + +```ts title="client.ts" +import { createClient } from './packages/paymesh/src'; +import { abacatepay } from './packages/abacatepay/src'; + +export const paymesh = createClient({ + provider: abacatepay({ + sandbox: true, + }), + mcp: { + readonly: true, + maxListLimit: 50, + tools: { + payments: false, + }, + }, +}); +``` + +## What to expect + +The MCP server is designed to stay thin: + +- it imports your real Paymesh client +- it exposes a small set of tools that mirror client behavior +- it returns structured JSON instead of JSON-as-text +- it keeps provider-specific details inside the client layer +- `paymesh mcp` configures supported clients automatically instead of asking you to hand-write MCP config every time + +That makes it useful for support, finance, and operator workflows without adding a second billing model. diff --git a/apps/web/src/lib/docs-navigation.ts b/apps/web/src/lib/docs-navigation.ts index e196918..26b9231 100644 --- a/apps/web/src/lib/docs-navigation.ts +++ b/apps/web/src/lib/docs-navigation.ts @@ -196,7 +196,7 @@ export const docsNavigation: DocNavGroup[] = [ { label: 'MCP', icon: 'braces', - status: 'planned', + href: '/docs/mcp', }, { label: 'Skills', diff --git a/bun.lock b/bun.lock index 55a257a..cf73e7f 100644 --- a/bun.lock +++ b/bun.lock @@ -154,6 +154,26 @@ "paymesh": ">=0.0.0", }, }, + "packages/mcp": { + "name": "@paymesh/mcp", + "version": "0.0.0", + "bin": { + "paymesh-mcp": "./dist/cli.mjs", + }, + "dependencies": { + "@cfworker/json-schema": "^4.1.1", + "@modelcontextprotocol/server": "^2.0.0-alpha.2", + "commander": "^14.0.2", + "jiti": "^2.7.0", + "zod": "^4.4.3", + }, + "devDependencies": { + "paymesh": "workspace:*", + }, + "peerDependencies": { + "paymesh": ">=0.0.0", + }, + }, "packages/next": { "name": "@paymesh/next", "version": "0.0.0", @@ -252,6 +272,8 @@ "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], @@ -442,6 +464,8 @@ "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0-alpha.2", "", { "dependencies": { "zod": "^4.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmLgdHzlYM8L7Aw/+VE0kxjT25WKamtUSLNhdOgrJq5CrESvqVSoAfWSJJeNPUXNTluQ+dYDGFbKVitdsJtbPA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@next/env": ["@next/env@16.2.7", "", {}, "sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg=="], @@ -490,6 +514,8 @@ "@paymesh/hono": ["@paymesh/hono@workspace:packages/hono"], + "@paymesh/mcp": ["@paymesh/mcp@workspace:packages/mcp"], + "@paymesh/next": ["@paymesh/next@workspace:packages/next"], "@paymesh/polar": ["@paymesh/polar@workspace:packages/polar"], diff --git a/package.json b/package.json index 20e3cbb..1a7a811 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "check": "biome check .", "check:write": "biome check --write .", "typecheck": "bun run typecheck:packages", - "typecheck:packages": "bun run --filter './packages/{paymesh,audit-logs,postgres,drizzle,prisma,stripe,polar,abacatepay,express,fastify,hono,elysia,next,cli}' typecheck", + "typecheck:packages": "bun run --filter './packages/{paymesh,audit-logs,postgres,drizzle,prisma,stripe,polar,abacatepay,express,fastify,hono,elysia,next,cli,mcp}' typecheck", "typecheck:apps": "bun run --filter './apps/*' typecheck", "types": "bun run typecheck", "test": "bun test --pass-with-no-tests", diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts new file mode 100644 index 0000000..786fccf --- /dev/null +++ b/packages/cli/src/commands/mcp.ts @@ -0,0 +1,233 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { cancel, isCancel, select } from '@clack/prompts'; +import type { Command } from 'commander'; +import { resolveClientPath } from 'src/lib/client'; +import { + formatPath, + logInfo, + logSuccess, + logTitle, + logWarning, +} from 'src/lib/output'; +import { + buildMcpServerConfig, + createCursorDeeplink, + formatCodexToml, + formatMcpCommand, + type McpTarget, + openUrl, + readJsonFile, + writeJsonFile, +} from 'src/shared/mcp'; + +const SERVER_NAME = 'paymesh'; + +export function registerMcpCommand(program: Command) { + return program + .command('mcp') + .description('Configure Paymesh for MCP-capable clients') + .option('--cwd ', 'Working directory', process.cwd()) + .option('--client ', 'Path to the Paymesh client module') + .option('--export ', 'Named export to load from the client module') + .option('--cursor', 'Configure Cursor', false) + .option('--claude-code', 'Configure Claude Code', false) + .option('--codex', 'Configure Codex', false) + .option('--open-code', 'Configure Open Code', false) + .option('--manual', 'Print the generated MCP config', false) + .action(async (options) => { + const cwd = path.resolve(options.cwd); + const clientPath = await resolveClientPath(cwd, options.client); + const exportName = options.export as string | undefined; + const target = resolveTarget(options) ?? (await chooseTarget()); + const serverConfig = buildMcpServerConfig({ + clientPath, + exportName, + }); + const install = { + target, + clientPath, + exportName, + serverConfig, + }; + + switch (target) { + case 'cursor': + await setupCursor(install); + return; + case 'claude-code': + await setupClaudeCode(install); + return; + case 'codex': + await setupCodex(install); + return; + case 'open-code': + await setupOpenCode(cwd, install); + return; + case 'manual': + printManualConfig(install); + return; + } + }); +} + +function resolveTarget(options: { + cursor?: boolean; + claudeCode?: boolean; + codex?: boolean; + openCode?: boolean; + manual?: boolean; +}) { + const selected = [ + options.cursor ? 'cursor' : null, + options.claudeCode ? 'claude-code' : null, + options.codex ? 'codex' : null, + options.openCode ? 'open-code' : null, + options.manual ? 'manual' : null, + ].filter(Boolean) as McpTarget[]; + + if (selected.length === 1) return selected[0]!; + + if (selected.length > 1) + throw new Error('Choose only one MCP target at a time.'); + + return null; +} + +async function chooseTarget() { + const target = await select({ + message: 'Which client do you want to configure?', + options: [ + { value: 'cursor', label: 'Cursor' }, + { value: 'claude-code', label: 'Claude Code' }, + { value: 'codex', label: 'Codex' }, + { value: 'open-code', label: 'Open Code' }, + { value: 'manual', label: 'Manual JSON' }, + ], + }); + + if (isCancel(target)) { + cancel('Cancelled.'); + process.exit(0); + } + + return target as McpTarget; +} + +async function setupCursor({ + serverConfig, +}: { + serverConfig: { command: string; args: string[] }; +}) { + const deeplink = createCursorDeeplink(SERVER_NAME, serverConfig); + + logTitle('Cursor MCP', 'deeplink install'); + logInfo(`Opening ${formatPath('Cursor')} install link`); + + const opened = openUrl(deeplink); + if (opened.status !== 0) { + logWarning('Could not open Cursor automatically.'); + console.log(deeplink); + return; + } + + logSuccess('Cursor should prompt to add the Paymesh MCP server.'); +} + +async function setupClaudeCode({ + serverConfig, +}: { + serverConfig: { command: string; args: string[] }; +}) { + const command = { + command: 'claude', + args: [ + 'mcp', + 'add', + SERVER_NAME, + '--', + serverConfig.command, + ...serverConfig.args, + ], + }; + + logTitle('Claude Code MCP', 'automatic install'); + + const result = spawnSync(command.command, command.args, { + stdio: 'inherit', + }); + + if (result.status === 0) { + logSuccess('Claude Code configured successfully.'); + return; + } + + logWarning('Claude Code install failed. Run this command manually:'); + console.log(formatMcpCommand(command)); +} + +async function setupCodex({ + serverConfig, +}: { + serverConfig: { command: string; args: string[] }; +}) { + const command = { + command: 'codex', + args: [ + 'mcp', + 'add', + SERVER_NAME, + '--', + serverConfig.command, + ...serverConfig.args, + ], + }; + + logTitle('Codex MCP', 'automatic install'); + + const result = spawnSync(command.command, command.args, { + stdio: 'inherit', + }); + + if (result.status === 0) { + logSuccess('Codex configured successfully.'); + return; + } + + logWarning('Codex install failed. Run this config manually:'); + console.log(formatCodexToml(serverConfig)); +} + +async function setupOpenCode( + cwd: string, + { + serverConfig, + }: { + serverConfig: { command: string; args: string[] }; + }, +) { + const filePath = path.join(cwd, 'opencode.json'); + const existing = await readJsonFile>(filePath); + const next = { + ...(existing ?? {}), + mcpServers: { + ...(((existing?.mcpServers as Record | undefined) ?? + {}) as Record), + [SERVER_NAME]: serverConfig, + }, + }; + + await writeJsonFile(filePath, next); + + logTitle('Open Code MCP', 'configuration updated'); + logSuccess(`Updated ${formatPath(path.relative(cwd, filePath))}`); +} + +function printManualConfig({ + serverConfig, +}: { + serverConfig: { command: string; args: string[] }; +}) { + logTitle('Manual MCP config'); + console.log(formatCodexToml(serverConfig)); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 3e91b99..5c44ee4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { registerGenerateCommand } from './commands/generate'; import { registerInitCommand } from './commands/init'; import { registerListenCommand } from './commands/listen'; +import { registerMcpCommand } from './commands/mcp'; import { registerMigrateCommand } from './commands/migrate'; import { registerPluginsCommand } from './commands/plugins'; import { registerPushCommand } from './commands/push'; @@ -35,7 +36,7 @@ import packageJson from 'package.json'; export { type CliStatus, getPaymeshStatus } from './lib/status'; -export const version = packageJson.version; +export const { version } = packageJson; export function createProgram() { const program = new Command(); @@ -52,6 +53,7 @@ export function createProgram() { registerInitCommand(program); registerListenCommand(program); registerMigrateCommand(program); + registerMcpCommand(program); registerPushCommand(program); registerStatusCommand(program); registerPluginsCommand(program); diff --git a/packages/cli/src/shared/mcp.ts b/packages/cli/src/shared/mcp.ts new file mode 100644 index 0000000..8700edf --- /dev/null +++ b/packages/cli/src/shared/mcp.ts @@ -0,0 +1,78 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync, promises as fs } from 'node:fs'; +import path from 'node:path'; + +export type McpTarget = + | 'cursor' + | 'claude-code' + | 'codex' + | 'open-code' + | 'manual'; + +export function buildMcpServerConfig({ + clientPath, + exportName, +}: { + clientPath: string; + exportName?: string; +}) { + const args = ['-y', '@paymesh/mcp', '--client', clientPath]; + + if (exportName) args.push('--export', exportName); + + return { + command: 'npx', + args, + }; +} + +export function createCursorDeeplink( + serverName: string, + config: { + command: string; + args: string[]; + }, +) { + const encoded = Buffer.from(JSON.stringify(config)).toString('base64'); + return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(serverName)}&config=${encodeURIComponent(encoded)}`; +} + +export async function readJsonFile(filePath: string): Promise { + if (!existsSync(filePath)) return null; + + try { + return JSON.parse(await fs.readFile(filePath, 'utf8')) as T; + } catch { + return null; + } +} + +export async function writeJsonFile(filePath: string, value: unknown) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +export function openUrl(url: string) { + const platform = process.platform; + const command = + platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open'; + const args = platform === 'win32' ? ['/c', 'start', '', url] : [url]; + + return spawnSync(command, args, { stdio: 'ignore' }); +} + +export function formatMcpCommand(config: { command: string; args: string[] }) { + return [config.command, ...config.args] + .map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)) + .join(' '); +} + +export function formatCodexToml(config: { command: string; args: string[] }) { + const args = config.args.map((arg) => JSON.stringify(arg)).join(', '); + + return [ + '[mcp_servers.paymesh]', + `command = ${JSON.stringify(config.command)}`, + `args = [${args}]`, + ].join('\n'); +} diff --git a/packages/cli/test/cli.test.ts b/packages/cli/test/cli.test.ts index 56525b1..dca3548 100644 --- a/packages/cli/test/cli.test.ts +++ b/packages/cli/test/cli.test.ts @@ -141,7 +141,7 @@ describe('cli helpers', () => { expect(logs).toContain('status 200'); expect(listenerLine).toContain('source=trigger'); expect(listenerLine).toContain('event=payment.succeeded'); - await expect( + expect( fs.readFile(path.join(directory, 'trigger-log.json'), 'utf8'), ).rejects.toThrow(); } finally { @@ -231,11 +231,100 @@ describe('cli helpers', () => { ).toContain('"code":"FILE10"'); }); + test('prints a manual MCP config', async () => { + const directory = await createTempProject(); + await fs.writeFile( + path.join(directory, 'paymesh-client.ts'), + 'export default {};', + ); + + const output = await withinCwd(directory, () => + captureLogs(async () => { + await createProgram().parseAsync([ + 'node', + 'paymesh', + 'mcp', + '--manual', + '--client', + './paymesh-client.ts', + '--export', + 'paymesh', + ]); + }), + ); + + expect(output).toContain('[mcp_servers.paymesh]'); + expect(output).toContain('command = "npx"'); + expect(output).toContain('"@paymesh/mcp"'); + expect(output).toContain('"--export"'); + expect(output).toContain('"paymesh"'); + }); + + test('prints a Codex MCP config', async () => { + const directory = await createTempProject(); + await fs.writeFile( + path.join(directory, 'paymesh-client.ts'), + 'export default {};', + ); + + const output = await withinCwd(directory, () => + captureLogs(async () => { + await createProgram().parseAsync([ + 'node', + 'paymesh', + 'mcp', + '--codex', + '--client', + './paymesh-client.ts', + ]); + }), + ); + + expect(output).toSatisfy( + (value) => + value.includes('Codex configured successfully.') || + value.includes('[mcp_servers.paymesh]'), + ); + }); + + test('updates opencode.json when configuring Open Code', async () => { + const directory = await createTempProject(); + await fs.writeFile( + path.join(directory, 'paymesh-client.ts'), + 'export default {};', + ); + + await withinCwd(directory, () => + captureLogs(async () => { + await createProgram().parseAsync([ + 'node', + 'paymesh', + 'mcp', + '--open-code', + '--client', + './paymesh-client.ts', + ]); + }), + ); + + const opencode = JSON.parse( + await fs.readFile(path.join(directory, 'opencode.json'), 'utf8'), + ) as { + mcpServers: Record; + }; + + expect(opencode.mcpServers.paymesh!.command).toBe('npx'); + expect(opencode.mcpServers.paymesh!.args).toContain('@paymesh/mcp'); + expect(opencode.mcpServers.paymesh!.args).toContain( + path.join(directory, 'paymesh-client.ts'), + ); + }); + test('rejects non-object built-in --data payloads', async () => { const directory = await createTempProject(); await writeCliClient(directory); - await expect( + expect( withinCwd(directory, () => createProgram().parseAsync([ 'node', @@ -258,7 +347,7 @@ describe('cli helpers', () => { const directory = await createTempProject(); await writeCliClient(directory); - await expect( + expect( withinCwd(directory, () => createProgram().parseAsync([ 'node', @@ -281,7 +370,7 @@ describe('cli helpers', () => { const directory = await createTempProject(); await writeCliClient(directory); - await expect( + expect( withinCwd(directory, () => createProgram().parseAsync([ 'node', @@ -303,7 +392,7 @@ describe('cli helpers', () => { const directory = await createTempProject(); await writeCliClient(directory); - await expect( + expect( withinCwd(directory, () => createProgram().parseAsync([ 'node', @@ -452,7 +541,7 @@ describe('cli helpers', () => { const summaryLine = stripAnsi(lines[0] ?? ''); expect(response.status).toBe(405); - await expect(response.json()).resolves.toEqual({ + expect(response.json()).resolves.toEqual({ error: 'method_not_allowed', }); expect(summaryLine).toContain('405'); @@ -463,7 +552,7 @@ describe('cli helpers', () => { }); test('fails to start the listener when provider does not support webhooks', async () => { - await expect( + expect( startWebhookServer({ client: { provider: defineProvider({ diff --git a/packages/drizzle/src/repositories.ts b/packages/drizzle/src/repositories.ts index 7ec88c7..6b371e6 100644 --- a/packages/drizzle/src/repositories.ts +++ b/packages/drizzle/src/repositories.ts @@ -74,6 +74,72 @@ export function createRepositories( options?.includeRaw, ) as never; }, + async findByEmail(schema, provider, sandbox, email, options) { + const fields = Object.values(schema.tables.customers.fields); + const [row] = await executor.query< + { + provider: string; + provider_id: string; + sandbox: boolean | null; + data: Record | null; + raw: unknown; + } & Record + >({ + sql: `SELECT provider, provider_id, sandbox, data, raw${fields.length === 0 ? '' : `, ${fields.map((field) => `${quoteIdentifier(field.column)} AS ${quoteIdentifier(field.key)}`).join(', ')}`} + FROM ${tableName(schema, 'customers')} + WHERE provider = $1 AND sandbox = $2 AND email = $3 AND deleted_at IS NULL + LIMIT 1`, + params: [provider, sandbox, email], + }); + + if (!row) return null; + + const data = hydrateStoredData(row.data, fields, row); + + return withRaw( + { + ...data, + id: row.provider_id, + provider: row.provider, + sandbox: row.sandbox ?? false, + }, + row.raw, + options?.includeRaw, + ) as never; + }, + async findByExternalId(schema, provider, sandbox, externalId, options) { + const fields = Object.values(schema.tables.customers.fields); + const [row] = await executor.query< + { + provider: string; + provider_id: string; + sandbox: boolean | null; + data: Record | null; + raw: unknown; + } & Record + >({ + sql: `SELECT provider, provider_id, sandbox, data, raw${fields.length === 0 ? '' : `, ${fields.map((field) => `${quoteIdentifier(field.column)} AS ${quoteIdentifier(field.key)}`).join(', ')}`} + FROM ${tableName(schema, 'customers')} + WHERE provider = $1 AND sandbox = $2 AND external_id = $3 AND deleted_at IS NULL + LIMIT 1`, + params: [provider, sandbox, externalId], + }); + + if (!row) return null; + + const data = hydrateStoredData(row.data, fields, row); + + return withRaw( + { + ...data, + id: row.provider_id, + provider: row.provider, + sandbox: row.sandbox ?? false, + }, + row.raw, + options?.includeRaw, + ) as never; + }, async list(schema, provider, sandbox, options) { const fields = Object.values(schema.tables.customers.fields); const limit = options?.limit ?? 20; diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 0000000..91eceac --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,104 @@ +
+ +

@paymesh/mcp

+ +

+ Turn your Paymesh client into an MCP server, with the same runtime config your app already uses. +

+ +

+ Expose customers, payments, PIX, and plugin introspection to AI tools through a small MCP server that reads your real Paymesh client instead of a parallel config file. +

+ +

+ Installation · + Usage · + Configure from the CLI · + Available tools · + Why use it +

+ +
+ +

Installation

+ +```bash +npm install paymesh @paymesh/mcp +``` + +

Usage

+ +```ts +import { createClient } from "paymesh"; +import { stripe } from "@paymesh/stripe"; + +export const billing = createClient({ + provider: stripe({ + secret: process.env.STRIPE_API_KEY!, + webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, + }), + mcp: { + readonly: true, + maxListLimit: 50, + tools: { + customers: true, + payments: true, + pix: true, + plugins: true, + }, + }, +}); +``` + +```bash +npx @paymesh/mcp \ + --client ./src/lib/paymesh.ts \ + --export billing \ + --readonly \ + --max-list-limit 50 +``` + +

+ The final MCP config is resolved from Paymesh defaults, then client.$mcp, then CLI flags. That keeps the MCP server aligned with the same client module your application imports. +

+ +

Configure from the CLI

+ +

+ The paymesh mcp command configures supported MCP clients for you. +

+ +```bash +paymesh mcp \ + --client ./src/lib/paymesh.ts \ + --export billing \ + --codex +``` + +

+ You can also use --cursor, --claude-code, --open-code, or --manual. When no target flag is passed, the CLI opens an interactive picker. +

+ +

Available Tools

+ +

+ @paymesh/mcp exposes customers_list, customers_get, customers_upsert, customers_delete, payments_create, pix_create, pix_get, and plugins_list. +

+ +

+ It also exposes capabilities_list, provider_info, customers_get_by_email, and customers_get_by_external_id. +

+ +

+ Readonly mode automatically hides mutating tools, and live-mode providers stay blocked unless you explicitly enable allowLiveMode. +

+ +

Why Use It

+ +

+ @paymesh/mcp lets AI tooling talk to your billing layer through the exact Paymesh client your product already trusts. No duplicated provider credentials, no second schema, and no separate MCP-specific business logic. +

+ +

+ That keeps the MCP surface small, predictable, and aligned with your real application runtime. +

diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000..8a0406e --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,46 @@ +{ + "name": "@paymesh/mcp", + "version": "0.0.0", + "description": "Official MCP package for Paymesh.", + "type": "module", + "license": "MIT", + "sideEffects": false, + "files": [ + "dist" + ], + "bin": { + "paymesh-mcp": "./dist/cli.mjs" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "scripts": { + "build": "tsdown", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "paymesh": ">=0.0.0" + }, + "devDependencies": { + "paymesh": "workspace:*" + }, + "dependencies": { + "@cfworker/json-schema": "^4.1.1", + "@modelcontextprotocol/server": "^2.0.0-alpha.2", + "commander": "^14.0.2", + "jiti": "^2.7.0", + "zod": "^4.4.3" + } +} diff --git a/packages/mcp/src/cli.ts b/packages/mcp/src/cli.ts new file mode 100644 index 0000000..089cd9f --- /dev/null +++ b/packages/mcp/src/cli.ts @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +import { StdioServerTransport } from '@modelcontextprotocol/server'; +import { Command } from 'commander'; +import packageJson from 'package.json'; +import { PaymeshError } from 'paymesh'; +import { createMcpServer } from './server'; +import { loadPaymeshClient } from './shared/client'; +import { resolveMcpConfig } from './shared/config'; +import type { PaymeshMcpCliOptions } from './types'; + +export function createProgram() { + const program = new Command(); + + configureProgram(program); + + return program; +} + +export function configureProgram(program: Command) { + program + .name('paymesh-mcp') + .description('Expose a Paymesh client as an MCP server over stdio') + .version(packageJson.version) + .showHelpAfterError() + .option( + '--client ', + 'Path to the module exporting the Paymesh client', + ) + .option( + '--export ', + 'Named export to load from the client module. Defaults to default or "paymesh".', + ) + .option('--enabled', 'Enable the MCP server') + .option('--no-enabled', 'Disable the MCP server') + .option('--readonly', 'Expose only readonly MCP tools') + .option('--no-readonly', 'Allow mutating MCP tools') + .option('--include-raw', 'Include raw provider payloads by default') + .option('--no-include-raw', 'Hide raw provider payloads by default') + .option( + '--allow-live-mode', + 'Allow startup and requests against live-mode providers', + ) + .option( + '--no-allow-live-mode', + 'Block startup and requests against live-mode providers', + ) + .option( + '--max-list-limit ', + 'Maximum allowed limit for list tools', + parsePositiveInteger, + ) + .option( + '--max-list-lines ', + 'Alias for --max-list-limit', + parsePositiveInteger, + ) + .option('--customers', 'Enable customer tools') + .option('--no-customers', 'Disable customer tools') + .option('--payments', 'Enable payment tools') + .option('--no-payments', 'Disable payment tools') + .option('--pix', 'Enable PIX tools') + .option('--no-pix', 'Disable PIX tools') + .option('--plugins', 'Enable plugin tools') + .option('--no-plugins', 'Disable plugin tools') + .action(async (options: Record) => { + await startServerFromCli(mapCommandOptions(options)); + }); + + return program; +} + +export function parseCliOptions( + raw: Record, +): PaymeshMcpCliOptions { + return mapCommandOptions(raw); +} + +export async function main(argv = process.argv) { + await createProgram().parseAsync(argv); +} + +export async function startServerFromCli(cliOptions: PaymeshMcpCliOptions) { + const client = await loadPaymeshClient({ + cwd: process.cwd(), + clientPath: cliOptions.client, + exportName: cliOptions.export, + }); + const config = resolveMcpConfig(client, cliOptions); + const server = createMcpServer({ client, config }); + const transport = new StdioServerTransport(); + + await server.connect(transport); + await waitForShutdown(async () => { + await server.close(); + await client.database?.close?.(); + }); +} + +function mapCommandOptions(raw: Record): PaymeshMcpCliOptions { + return { + client: asOptionalString(raw.client), + export: asOptionalString(raw.export), + enabled: asOptionalBoolean(raw.enabled), + readonly: asOptionalBoolean(raw.readonly), + includeRaw: asOptionalBoolean(raw.includeRaw), + allowLiveMode: asOptionalBoolean(raw.allowLiveMode), + maxListLimit: asOptionalNumber(raw.maxListLimit), + maxListLines: asOptionalNumber(raw.maxListLines), + tools: { + customers: asOptionalBoolean(raw.customers), + payments: asOptionalBoolean(raw.payments), + pix: asOptionalBoolean(raw.pix), + plugins: asOptionalBoolean(raw.plugins), + }, + }; +} + +function parsePositiveInteger(value: string) { + const parsed = Number.parseInt(value, 10); + + if (!Number.isInteger(parsed) || parsed < 1) { + throw new PaymeshError({ + code: 'cli_error', + message: `Invalid integer "${value}". Expected a positive integer.`, + }); + } + + return parsed; +} + +async function waitForShutdown(close: () => Promise) { + await new Promise((resolve, reject) => { + let closed = false; + + const finish = async (error?: unknown) => { + if (closed) return; + closed = true; + process.off('SIGINT', handleSignal); + process.off('SIGTERM', handleSignal); + process.off('uncaughtException', handleError); + + try { + await close(); + if (error) { + reject(error); + return; + } + resolve(); + } catch (closeError) { + reject(closeError); + } + }; + + const handleSignal = () => { + void finish(); + }; + const handleError = (error: unknown) => { + void finish(error); + }; + + process.on('SIGINT', handleSignal); + process.on('SIGTERM', handleSignal); + process.on('uncaughtException', handleError); + }); +} + +function asOptionalString(value: unknown) { + return typeof value === 'string' ? value : undefined; +} + +function asOptionalBoolean(value: unknown) { + return typeof value === 'boolean' ? value : undefined; +} + +function asOptionalNumber(value: unknown) { + return typeof value === 'number' ? value : undefined; +} + +if (isCliEntrypoint(process.argv[1])) { + void main(); +} + +function isCliEntrypoint(entrypoint?: string) { + if (!entrypoint) return false; + + return /(?:^|\/)(paymesh-mcp|cli)(?:\.[cm]?[jt]s)?$/.test(entrypoint); +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 0000000..6db1107 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,28 @@ +import { + configureProgram, + createProgram, + main, + parseCliOptions, + startServerFromCli, +} from './cli'; +import { createMcpServer, getRegisteredTools } from './server'; +import { loadPaymeshClient, resolveClientPath } from './shared/client'; +import { resolveMcpConfig } from './shared/config'; +import { assertServerCanStart } from './shared/tool'; +import { DEFAULT_MCP_CONFIG } from './types'; + +export type * from './types'; +export { + assertServerCanStart, + configureProgram, + createMcpServer, + createProgram, + DEFAULT_MCP_CONFIG, + getRegisteredTools, + loadPaymeshClient, + main, + parseCliOptions, + resolveClientPath, + resolveMcpConfig, + startServerFromCli, +}; diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts new file mode 100644 index 0000000..a90514d --- /dev/null +++ b/packages/mcp/src/server.ts @@ -0,0 +1,44 @@ +import { McpServer } from '@modelcontextprotocol/server'; +import packageJson from 'package.json'; +import type { PaymeshClient } from 'paymesh'; +import { resolveMcpConfig } from './shared/config'; +import { assertServerCanStart } from './shared/tool'; +import { getTools } from './tools'; +import type { CreatePaymeshMcpServerOptions } from './types'; + +export function createMcpServer({ + client, + config = resolveMcpConfig(client), +}: CreatePaymeshMcpServerOptions) { + assertServerCanStart(client, config); + + const server = new McpServer({ + name: 'paymesh', + title: 'Paymesh Billing MCP', + version: packageJson.version, + websiteUrl: 'https://paymesh-six.vercel.app/', + description: + 'Local MCP server for inspecting billing data from the current Paymesh project', + }); + + for (const tool of getTools({ client, config })) { + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: tool.inputSchema, + }, + tool.handler, + ); + } + + return server; +} + +export function getRegisteredTools( + client: PaymeshClient, + config = resolveMcpConfig(client), +) { + assertServerCanStart(client, config); + return getTools({ client, config }); +} diff --git a/packages/mcp/src/shared/client.ts b/packages/mcp/src/shared/client.ts new file mode 100644 index 0000000..aca8744 --- /dev/null +++ b/packages/mcp/src/shared/client.ts @@ -0,0 +1,79 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { createJiti } from 'jiti'; +import { type PaymeshClient, PaymeshError } from 'paymesh'; +import type { LoadPaymeshClientOptions } from '../types'; + +export async function loadPaymeshClient({ + cwd, + clientPath, + exportName, +}: LoadPaymeshClientOptions) { + const resolvedPath = await resolveClientPath(cwd, clientPath); + const module = (await loadModule(cwd, resolvedPath)) as Record< + string, + unknown + >; + + const client = exportName + ? module[exportName] + : (module.default ?? module.paymesh); + + if ( + typeof client !== 'object' || + client === null || + !('provider' in client) || + !('schema' in client) + ) + throw new PaymeshError({ + code: 'client_error', + message: `The client module "${resolvedPath}" must export the Paymesh client as ${exportName ? `named export "${exportName}"` : 'default export or named export "paymesh"'}.`, + }); + + return client as PaymeshClient; +} + +export async function resolveClientPath(cwd: string, explicitPath?: string) { + const candidate = + explicitPath ?? + process.env.PAYMESH_PATH ?? + (await readClientPathFromPackageJson(cwd)); + + if (!candidate) + throw new PaymeshError({ + code: 'client_error', + message: + 'Unable to resolve the Paymesh client path. Use --client, PAYMESH_PATH, or package.json.paymesh.path.', + }); + + return path.resolve(cwd, candidate); +} + +async function loadModule(cwd: string, resolvedPath: string) { + if (/\.(?:[cm]?ts|tsx)$/.test(resolvedPath)) { + const jiti = createJiti(path.join(cwd, 'package.json'), { + interopDefault: false, + }); + + return jiti.import(resolvedPath); + } + + return import(pathToFileURL(resolvedPath).href); +} + +async function readClientPathFromPackageJson(cwd: string) { + try { + const packageJson = JSON.parse( + await fs.readFile(path.join(cwd, 'package.json'), 'utf8'), + ) as { + paymesh?: { + path?: string; + }; + }; + + return packageJson.paymesh?.path; + } catch { + return undefined; + } +} diff --git a/packages/mcp/src/shared/config.ts b/packages/mcp/src/shared/config.ts new file mode 100644 index 0000000..03a38b9 --- /dev/null +++ b/packages/mcp/src/shared/config.ts @@ -0,0 +1,74 @@ +import type { PaymeshClient } from 'paymesh'; +import { PaymeshError } from 'paymesh'; +import { + DEFAULT_MCP_CONFIG, + MCP_TOOL_AREAS, + type PaymeshMcpCliOptions, + type PaymeshMcpToolArea, + type ResolvedPaymeshMcpConfig, +} from '../types'; + +export function resolveMcpConfig( + client: Pick, '$mcp'>, + cliOverrides: PaymeshMcpCliOptions = {}, +): ResolvedPaymeshMcpConfig { + const tools = cliOverrides.tools ?? {}; + const metadata = client.$mcp ?? DEFAULT_MCP_CONFIG; + const maxListLimit = cliOverrides.maxListLimit ?? cliOverrides.maxListLines; + + if (metadata.tools) { + const unsupported = Object.entries(metadata.tools) + .filter( + ([key, enabled]) => + !MCP_TOOL_AREAS.includes(key as PaymeshMcpToolArea) && enabled, + ) + .map(([key]) => key) + .sort(); + + if (unsupported.length > 0) + throw new PaymeshError({ + code: 'mcp_error', + message: `Unsupported MCP tool areas requested by the client: ${unsupported.join(', ')}.`, + }); + } + + const config = { + enabled: + cliOverrides.enabled ?? metadata.enabled ?? DEFAULT_MCP_CONFIG.enabled, + readonly: + cliOverrides.readonly ?? metadata.readonly ?? DEFAULT_MCP_CONFIG.readonly, + includeRaw: + cliOverrides.includeRaw ?? + metadata.includeRaw ?? + DEFAULT_MCP_CONFIG.includeRaw, + allowLiveMode: + cliOverrides.allowLiveMode ?? + metadata.allowLiveMode ?? + DEFAULT_MCP_CONFIG.allowLiveMode, + maxListLimit: + maxListLimit ?? metadata.maxListLimit ?? DEFAULT_MCP_CONFIG.maxListLimit, + tools: { + customers: + tools.customers ?? + metadata.tools?.customers ?? + DEFAULT_MCP_CONFIG.tools.customers, + payments: + tools.payments ?? + metadata.tools?.payments ?? + DEFAULT_MCP_CONFIG.tools.payments, + pix: tools.pix ?? metadata.tools?.pix ?? DEFAULT_MCP_CONFIG.tools.pix, + plugins: + tools.plugins ?? + metadata.tools?.plugins ?? + DEFAULT_MCP_CONFIG.tools.plugins, + }, + } satisfies ResolvedPaymeshMcpConfig; + + if (!Number.isInteger(config.maxListLimit) || config.maxListLimit < 1) + throw new PaymeshError({ + code: 'mcp_error', + message: `Invalid MCP maxListLimit "${String(config.maxListLimit)}". Expected a positive integer.`, + }); + + return config; +} diff --git a/packages/mcp/src/shared/schema.ts b/packages/mcp/src/shared/schema.ts new file mode 100644 index 0000000..d1eecaf --- /dev/null +++ b/packages/mcp/src/shared/schema.ts @@ -0,0 +1,30 @@ +import * as z from 'zod/v4'; + +export const requestOptionsSchema = z.object({ + includeRaw: z.boolean().optional(), + sandbox: z.boolean().optional(), +}); + +export const dataInputSchema = requestOptionsSchema.extend({ + data: z.looseObject({}), +}); + +export const idInputSchema = requestOptionsSchema.extend({ + id: z.string().min(1), +}); + +export const emailInputSchema = requestOptionsSchema.extend({ + email: z.email(), +}); + +export const externalIdInputSchema = requestOptionsSchema.extend({ + externalId: z.string().min(1), +}); + +export const customersListSchema = requestOptionsSchema.extend({ + limit: z.number().int().positive().optional(), + after: z.string().min(1).optional(), + before: z.string().min(1).optional(), +}); + +export const emptyInputSchema = z.object({}); diff --git a/packages/mcp/src/shared/tool.ts b/packages/mcp/src/shared/tool.ts new file mode 100644 index 0000000..277d333 --- /dev/null +++ b/packages/mcp/src/shared/tool.ts @@ -0,0 +1,69 @@ +import type { CallToolResult } from '@modelcontextprotocol/server'; +import { type PaymeshClient, PaymeshError } from 'paymesh'; +import type { ResolvedPaymeshMcpConfig } from '../types'; + +export function assertServerCanStart( + client: PaymeshClient, + config: ResolvedPaymeshMcpConfig, +) { + if (!config.enabled) + throw new PaymeshError({ + code: 'mcp_error', + message: 'The Paymesh MCP server is disabled by configuration.', + }); + + if (!config.allowLiveMode && !client.isSandbox()) + throw new PaymeshError({ + code: 'mcp_error', + message: + 'The Paymesh MCP server refuses to start in live mode unless allowLiveMode is enabled.', + provider: client.provider.id, + }); +} + +export async function runTool({ + run, + client, + config, + sandbox, +}: { + sandbox?: boolean; + run: () => Promise; + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}) { + const targetSandbox = sandbox ?? client.isSandbox(); + + if (!config.allowLiveMode && targetSandbox === false) + throw new PaymeshError({ + code: 'mcp_error', + message: + 'This MCP server blocks live-mode requests. Enable allowLiveMode to override it.', + provider: client.provider.id, + }); + + try { + return await run(); + } catch (error) { + throw PaymeshError.wrap(error, { + code: 'mcp_error', + message: + error instanceof Error + ? error.message + : 'The MCP tool execution failed.', + }); + } +} + +export function createToolResult(result: unknown): CallToolResult { + const structuredContent = JSON.parse( + JSON.stringify(result, (_key, value) => + value instanceof Date ? value.toISOString() : value, + ), + ) as Record; + + return { + content: [{ type: 'text', text: 'Structured JSON result.' }], + structuredContent, + }; +} diff --git a/packages/mcp/src/tools/capabilities.ts b/packages/mcp/src/tools/capabilities.ts new file mode 100644 index 0000000..4702f89 --- /dev/null +++ b/packages/mcp/src/tools/capabilities.ts @@ -0,0 +1,34 @@ +import type { PaymeshClient } from 'paymesh'; +import { emptyInputSchema } from '../shared/schema'; +import { createToolResult, runTool } from '../shared/tool'; +import type { PaymeshToolDefinition, ResolvedPaymeshMcpConfig } from '../types'; + +export function getCapabilitiesTools({ + client, + config, +}: { + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}): Array>> { + return [ + { + name: 'capabilities_list', + description: 'List the active Paymesh client capabilities.', + inputSchema: emptyInputSchema, + handler: async () => + createToolResult( + await runTool({ + client, + config, + run: () => + Promise.resolve({ + capabilities: Object.entries(client.capabilities) + .filter(([, enabled]) => enabled) + .map(([name]) => name) + .sort(), + }), + }), + ), + }, + ]; +} diff --git a/packages/mcp/src/tools/customers.ts b/packages/mcp/src/tools/customers.ts new file mode 100644 index 0000000..c9cbc83 --- /dev/null +++ b/packages/mcp/src/tools/customers.ts @@ -0,0 +1,305 @@ +import type { PaymeshClient } from 'paymesh'; +import { PaymeshError } from 'paymesh'; +import { + customersListSchema, + dataInputSchema, + emailInputSchema, + externalIdInputSchema, + idInputSchema, +} from '../shared/schema'; +import { createToolResult, runTool } from '../shared/tool'; +import type { PaymeshToolDefinition, ResolvedPaymeshMcpConfig } from '../types'; + +type CustomersListInput = { + limit?: number; + after?: string; + before?: string; + includeRaw?: boolean; + sandbox?: boolean; +}; + +type CustomerIdInput = { + id: string; + includeRaw?: boolean; + sandbox?: boolean; +}; + +type CustomerEmailInput = { + email: string; + includeRaw?: boolean; + sandbox?: boolean; +}; + +type CustomerExternalIdInput = { + externalId: string; + includeRaw?: boolean; + sandbox?: boolean; +}; + +type CustomerDataInput = { + data: Record; + includeRaw?: boolean; + sandbox?: boolean; +}; + +export function getCustomerTools({ + client, + config, +}: { + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}): Array>> { + const tools: Array>> = [ + { + name: 'customers_list', + description: 'List Paymesh customers.', + inputSchema: customersListSchema, + handler: async (input) => { + const values = input as CustomersListInput; + + if ( + typeof values.limit === 'number' && + values.limit > config.maxListLimit + ) + throw new PaymeshError({ + code: 'mcp_error', + message: `Requested list limit ${String(values.limit)} exceeds the configured maxListLimit ${String(config.maxListLimit)}.`, + }); + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + client.customers.list({ + limit: values.limit, + after: values.after, + before: values.before, + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + { + name: 'customers_get', + description: 'Get a Paymesh customer by id.', + inputSchema: idInputSchema, + handler: async (input) => { + const values = input as CustomerIdInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + client.customers.get(values.id, { + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + { + name: 'customers_get_by_email', + description: 'Get a Paymesh customer by email.', + inputSchema: emailInputSchema, + handler: async (input) => { + const values = input as CustomerEmailInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + resolveCustomerByField({ + client, + field: 'email', + value: values.email, + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + { + name: 'customers_get_by_external_id', + description: 'Get a Paymesh customer by external id.', + inputSchema: externalIdInputSchema, + handler: async (input) => { + const values = input as CustomerExternalIdInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + resolveCustomerByField({ + client, + field: 'externalId', + value: values.externalId, + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + ]; + + if (config.readonly) return tools; + + tools.push( + { + name: 'customers_upsert', + description: 'Create or update a Paymesh customer.', + inputSchema: dataInputSchema, + handler: async (input) => { + const values = input as CustomerDataInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + client.customers.upsert(values.data as never, { + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + { + name: 'customers_delete', + description: 'Delete a Paymesh customer by id.', + inputSchema: idInputSchema, + handler: async (input) => { + const values = input as CustomerIdInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + client.customers.delete(values.id, { + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + ); + + return tools; +} + +async function resolveCustomerByField({ + client, + field, + value, + includeRaw, + sandbox, +}: { + client: PaymeshClient; + field: 'email' | 'externalId'; + value: string; + includeRaw?: boolean; + sandbox?: boolean; +}) { + const database = client.database; + + if (!database) + throw new PaymeshError({ + code: 'unsupported_capability', + message: + 'Customer lookup by email or external id requires a configured database adapter.', + provider: client.provider.id, + }); + + const targetSandbox = sandbox ?? client.isSandbox(); + const repository = database.repositories.customers; + + if (field === 'email' && repository.findByEmail) { + const customer = await repository.findByEmail( + client.schema, + client.provider.id, + targetSandbox, + value, + { + includeRaw, + }, + ); + if (customer) return customer; + } else if (field === 'externalId' && repository.findByExternalId) { + const customer = await repository.findByExternalId( + client.schema, + client.provider.id, + targetSandbox, + value, + { + includeRaw, + }, + ); + if (customer) return customer; + } + + return findCustomerByField({ + list: (options) => + repository.list( + client.schema, + client.provider.id, + targetSandbox, + options, + ), + field, + value, + includeRaw, + }); +} + +async function findCustomerByField({ + list, + field, + value, + includeRaw, +}: { + list: (options?: { + limit?: number; + after?: string; + before?: string; + includeRaw?: boolean; + // biome-ignore lint/suspicious/noExplicitAny: any + }) => Promise; + field: 'email' | 'externalId'; + value: string; + includeRaw?: boolean; +}) { + let after: string | undefined; + + for (;;) { + const page = await list({ + limit: 100, + after, + includeRaw, + }); + + const match = page.data.find( + (customer: Record) => customer[field] === value, + ); + if (match) return match; + + if (!page.next) return null; + after = page.next; + } +} diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts new file mode 100644 index 0000000..1c1ac20 --- /dev/null +++ b/packages/mcp/src/tools/index.ts @@ -0,0 +1,39 @@ +import type { PaymeshClient } from 'paymesh'; +import type { PaymeshToolDefinition, ResolvedPaymeshMcpConfig } from '../types'; +import { getCapabilitiesTools } from './capabilities'; +import { getCustomerTools } from './customers'; +import { getPaymentTools } from './payments'; +import { getPixTools } from './pix'; +import { getPluginTools } from './plugins'; +import { getProviderInfoTools } from './provider-info'; + +export function getTools({ + client, + config, +}: { + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}): Array>> { + const tools: Array>> = []; + + tools.push(...getCapabilitiesTools({ client, config })); + tools.push(...getProviderInfoTools({ client, config })); + + if (config.tools.customers) { + tools.push(...getCustomerTools({ client, config })); + } + + if (config.tools.payments) { + tools.push(...getPaymentTools({ client, config })); + } + + if (config.tools.pix) { + tools.push(...getPixTools({ client, config })); + } + + if (config.tools.plugins) { + tools.push(...getPluginTools({ client, config })); + } + + return tools; +} diff --git a/packages/mcp/src/tools/payments.ts b/packages/mcp/src/tools/payments.ts new file mode 100644 index 0000000..f5331cf --- /dev/null +++ b/packages/mcp/src/tools/payments.ts @@ -0,0 +1,44 @@ +import type { PaymeshClient } from 'paymesh'; +import { dataInputSchema } from '../shared/schema'; +import { createToolResult, runTool } from '../shared/tool'; +import type { PaymeshToolDefinition, ResolvedPaymeshMcpConfig } from '../types'; + +type PaymentCreateInput = { + data: Record; + includeRaw?: boolean; + sandbox?: boolean; +}; + +export function getPaymentTools({ + client, + config, +}: { + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}): Array>> { + if (config.readonly) return []; + + return [ + { + name: 'payments_create', + description: 'Create a Paymesh payment.', + inputSchema: dataInputSchema, + handler: async (input) => { + const values = input as PaymentCreateInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + client.payments.create(values.data as never, { + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + ]; +} diff --git a/packages/mcp/src/tools/pix.ts b/packages/mcp/src/tools/pix.ts new file mode 100644 index 0000000..9a15b96 --- /dev/null +++ b/packages/mcp/src/tools/pix.ts @@ -0,0 +1,74 @@ +import type { PaymeshClient } from 'paymesh'; +import { dataInputSchema, idInputSchema } from '../shared/schema'; +import { createToolResult, runTool } from '../shared/tool'; +import type { PaymeshToolDefinition, ResolvedPaymeshMcpConfig } from '../types'; + +type PixInput = { + data: Record; + includeRaw?: boolean; + sandbox?: boolean; +}; + +type PixIdInput = { + id: string; + includeRaw?: boolean; + sandbox?: boolean; +}; + +export function getPixTools({ + client, + config, +}: { + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}): Array>> { + const tools: Array>> = [ + { + name: 'pix_get', + description: 'Get a Paymesh PIX payment by id.', + inputSchema: idInputSchema, + handler: async (input) => { + const values = input as PixIdInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + client.pix.get(values.id, { + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }, + ]; + + if (config.readonly) return tools; + + tools.unshift({ + name: 'pix_create', + description: 'Create a Paymesh PIX payment.', + inputSchema: dataInputSchema, + handler: async (input) => { + const values = input as PixInput; + + return createToolResult( + await runTool({ + client, + config, + sandbox: values.sandbox, + run: () => + client.pix.create(values.data as never, { + includeRaw: values.includeRaw ?? config.includeRaw, + sandbox: values.sandbox, + }), + }), + ); + }, + }); + + return tools; +} diff --git a/packages/mcp/src/tools/plugins.ts b/packages/mcp/src/tools/plugins.ts new file mode 100644 index 0000000..43217cb --- /dev/null +++ b/packages/mcp/src/tools/plugins.ts @@ -0,0 +1,28 @@ +import type { PaymeshClient } from 'paymesh'; +import { emptyInputSchema } from '../shared/schema'; +import { createToolResult, runTool } from '../shared/tool'; +import type { PaymeshToolDefinition, ResolvedPaymeshMcpConfig } from '../types'; + +export function getPluginTools({ + client, + config, +}: { + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}): Array>> { + return [ + { + name: 'plugins_list', + description: 'List registered Paymesh plugins.', + inputSchema: emptyInputSchema, + handler: async () => + createToolResult( + await runTool({ + client, + config, + run: () => Promise.resolve(client.plugins.list()), + }), + ), + }, + ]; +} diff --git a/packages/mcp/src/tools/provider-info.ts b/packages/mcp/src/tools/provider-info.ts new file mode 100644 index 0000000..a07a19c --- /dev/null +++ b/packages/mcp/src/tools/provider-info.ts @@ -0,0 +1,40 @@ +import type { PaymeshClient } from 'paymesh'; +import { emptyInputSchema } from '../shared/schema'; +import { createToolResult, runTool } from '../shared/tool'; +import type { PaymeshToolDefinition, ResolvedPaymeshMcpConfig } from '../types'; + +export function getProviderInfoTools({ + client, + config, +}: { + client: PaymeshClient; + config: ResolvedPaymeshMcpConfig; +}): Array>> { + return [ + { + name: 'provider_info', + description: 'Show the active provider, sandbox mode, and capabilities.', + inputSchema: emptyInputSchema, + handler: async () => + createToolResult( + await runTool({ + client, + config, + run: () => + Promise.resolve({ + client: { + sandbox: client.isSandbox(), + }, + provider: { + id: client.provider.id, + capabilities: Object.entries(client.capabilities) + .filter(([, enabled]) => enabled) + .map(([name]) => name) + .sort(), + }, + }), + }), + ), + }, + ]; +} diff --git a/packages/mcp/src/types/index.ts b/packages/mcp/src/types/index.ts new file mode 100644 index 0000000..f311126 --- /dev/null +++ b/packages/mcp/src/types/index.ts @@ -0,0 +1,65 @@ +import type { CallToolResult } from '@modelcontextprotocol/server'; +import type { PaymeshClient } from 'paymesh'; +import type { ZodObject } from 'zod/v4'; + +export const MCP_TOOL_AREAS = [ + 'customers', + 'payments', + 'pix', + 'plugins', +] as const; + +export type PaymeshMcpToolArea = (typeof MCP_TOOL_AREAS)[number]; + +export interface PaymeshMcpCliOptions { + client?: string; + export?: string; + enabled?: boolean; + readonly?: boolean; + includeRaw?: boolean; + allowLiveMode?: boolean; + maxListLimit?: number; + maxListLines?: number; + tools?: Partial>; +} + +export interface ResolvedPaymeshMcpConfig { + enabled: boolean; + readonly: boolean; + includeRaw: boolean; + allowLiveMode: boolean; + maxListLimit: number; + tools: Record; +} + +export interface LoadPaymeshClientOptions { + cwd: string; + clientPath?: string; + exportName?: string; +} + +export interface CreatePaymeshMcpServerOptions { + client: PaymeshClient; + config?: ResolvedPaymeshMcpConfig; +} + +export interface PaymeshToolDefinition> { + name: string; + description: string; + inputSchema: ZodObject; + handler: (input: TInput) => Promise; +} + +export const DEFAULT_MCP_CONFIG: ResolvedPaymeshMcpConfig = { + enabled: true, + readonly: false, + includeRaw: false, + allowLiveMode: false, + maxListLimit: 50, + tools: { + customers: true, + payments: true, + pix: true, + plugins: true, + }, +}; diff --git a/packages/mcp/test/mcp.test.ts b/packages/mcp/test/mcp.test.ts new file mode 100644 index 0000000..6f9a68b --- /dev/null +++ b/packages/mcp/test/mcp.test.ts @@ -0,0 +1,519 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test'; +import { promises as fs } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import type { PaymeshClient } from 'paymesh'; +import { + DEFAULT_MCP_CONFIG, + getRegisteredTools, + loadPaymeshClient, + parseCliOptions, + resolveClientPath, + resolveMcpConfig, +} from '../src'; +import { createToolResult } from '../src/shared/tool'; + +const tempDirectories: string[] = []; + +afterEach(async () => { + for (const directory of tempDirectories.splice(0)) { + await fs.rm(directory, { recursive: true, force: true }); + } +}); + +// biome-ignore lint/suspicious/noExplicitAny: Any +type Any = any; + +describe('@paymesh/mcp', () => { + test('resolves the client path from package.json', async () => { + const directory = await createTempProject(); + await fs.writeFile( + path.join(directory, 'package.json'), + JSON.stringify({ paymesh: { path: './src/paymesh-client.mjs' } }), + ); + await fs.mkdir(path.join(directory, 'src'), { recursive: true }); + await fs.writeFile( + path.join(directory, 'src/paymesh-client.mjs'), + 'export default {};', + ); + + expect(await resolveClientPath(directory)).toBe( + path.join(directory, 'src/paymesh-client.mjs'), + ); + }); + + test('loads default and named paymesh client exports', async () => { + const defaultDirectory = await createTempProject(); + await writeModule( + defaultDirectory, + 'default-client.mjs', + 'export default { provider: { id: "stub" }, schema: { prefix: "paymesh_", tables: {} } };', + ); + const namedDirectory = await createTempProject(); + await writeModule( + namedDirectory, + 'named-client.mjs', + 'export const billing = { provider: { id: "stub" }, schema: { prefix: "paymesh_", tables: {} } };', + ); + + const defaultClient = await loadPaymeshClient({ + cwd: defaultDirectory, + clientPath: './default-client.mjs', + }); + const namedClient = await loadPaymeshClient({ + cwd: namedDirectory, + clientPath: './named-client.mjs', + exportName: 'billing', + }); + + expect(defaultClient.provider.id).toBe('stub'); + expect(namedClient.provider.id).toBe('stub'); + }); + + test('loads a TypeScript client module', async () => { + const directory = await createTempProject(); + await writeModule( + directory, + 'ts-client.ts', + 'export const billing = { provider: { id: "stub" }, schema: { prefix: "paymesh_", tables: {} } };', + ); + + const client = await loadPaymeshClient({ + cwd: directory, + clientPath: './ts-client.ts', + exportName: 'billing', + }); + + expect(client.provider.id).toBe('stub'); + }); + + test('rejects invalid named exports', async () => { + const directory = await createTempProject(); + await writeModule( + directory, + 'named-client.mjs', + 'export const billing = { nope: true };', + ); + + expect( + loadPaymeshClient({ + cwd: directory, + clientPath: './named-client.mjs', + exportName: 'billing', + }), + ).rejects.toThrow('named export "billing"'); + }); + + test('merges client metadata with CLI overrides', () => { + const client = createTestClient({ + $mcp: { + enabled: true, + readonly: false, + includeRaw: false, + allowLiveMode: false, + maxListLimit: 20, + tools: { + customers: true, + payments: false, + pix: true, + plugins: true, + }, + }, + }); + + const config = resolveMcpConfig(client, { + readonly: true, + maxListLines: 10, + tools: { + payments: true, + plugins: false, + }, + }); + + expect(config).toMatchObject({ + readonly: true, + maxListLimit: 10, + tools: { + customers: true, + payments: true, + pix: true, + plugins: false, + }, + }); + }); + + test('rejects unsupported tool metadata when enabled', () => { + const client = createTestClient({ + $mcp: { + ...DEFAULT_MCP_CONFIG, + tools: { + ...DEFAULT_MCP_CONFIG.tools, + subscriptions: true, + }, + } as PaymeshClient['$mcp'], + }); + + expect(() => resolveMcpConfig(client)).toThrow( + 'Unsupported MCP tool areas requested by the client: subscriptions.', + ); + }); + + test('builds readonly tool definitions', () => { + const client = createTestClient(); + const tools = getRegisteredTools( + client, + resolveMcpConfig(client, { readonly: true }), + ); + + expect(tools.map((tool) => tool.name)).toEqual([ + 'capabilities_list', + 'provider_info', + 'customers_list', + 'customers_get', + 'customers_get_by_email', + 'customers_get_by_external_id', + 'pix_get', + 'plugins_list', + ]); + }); + + test('capabilities_list returns the enabled capabilities', async () => { + const client = createTestClient({ + capabilities: { + customers: true, + pix: false, + checkout: true, + }, + }); + const tools = getRegisteredTools(client, resolveMcpConfig(client)); + const tool = tools.find((entry) => entry.name === 'capabilities_list'); + + const result = await tool!.handler({}); + + expect(result.structuredContent).toEqual({ + capabilities: ['checkout', 'customers'], + }); + }); + + test('provider_info returns provider and sandbox details', async () => { + const client = createTestClient({ + isSandbox: () => false, + $mcp: { + ...DEFAULT_MCP_CONFIG, + allowLiveMode: true, + }, + provider: { + id: 'provider_x', + capabilities: { + customers: true, + pix: true, + }, + } as PaymeshClient['provider'], + capabilities: { + customers: true, + pix: true, + }, + }); + const tools = getRegisteredTools(client, resolveMcpConfig(client)); + const tool = tools.find((entry) => entry.name === 'provider_info'); + + const result = await tool!.handler({}); + + expect(result.structuredContent).toEqual({ + client: { + sandbox: false, + }, + provider: { + id: 'provider_x', + capabilities: ['customers', 'pix'], + }, + }); + }); + + test('customers_list delegates to client.customers.list', async () => { + const list = mock(() => + Promise.resolve({ + data: [{ id: 'cus_123' }], + total: 1, + next: null, + previous: null, + }), + ); + const client = createTestClient({ + customers: { + ...createTestClient().customers, + list, + } as unknown as PaymeshClient['customers'], + }); + const tools = getRegisteredTools(client, resolveMcpConfig(client)); + const tool = tools.find((entry) => entry.name === 'customers_list'); + + expect(tool).toBeDefined(); + + await tool!.handler({ limit: 25 }); + + expect(list).toHaveBeenCalledWith({ + after: undefined, + before: undefined, + includeRaw: false, + limit: 25, + sandbox: undefined, + }); + }); + + test('customers_get_by_email delegates to client.customers.getByEmail', async () => { + const findByEmail = mock(() => Promise.resolve({ id: 'cus_123' })); + const client = createTestClient({ + database: createTestClient().database, + }); + client.database!.repositories.customers.findByEmail = findByEmail as Any; + const tools = getRegisteredTools(client, resolveMcpConfig(client)); + const tool = tools.find((entry) => entry.name === 'customers_get_by_email'); + + await tool!.handler({ email: 'ada@example.com' }); + + expect(findByEmail).toHaveBeenCalledWith( + client.schema, + 'stub', + true, + 'ada@example.com', + { includeRaw: false }, + ); + }); + + test('customers_get_by_external_id delegates to client.customers.getByExternalId', async () => { + const findByExternalId = mock(() => Promise.resolve({ id: 'cus_123' })); + const client = createTestClient({ + database: createTestClient().database, + }); + client.database!.repositories.customers.findByExternalId = + findByExternalId as Any; + const tools = getRegisteredTools(client, resolveMcpConfig(client)); + const tool = tools.find( + (entry) => entry.name === 'customers_get_by_external_id', + ); + + await tool!.handler({ externalId: 'user_123' }); + + expect(findByExternalId).toHaveBeenCalledWith( + client.schema, + 'stub', + true, + 'user_123', + { includeRaw: false }, + ); + }); + + test('payments_create delegates to client.payments.create', async () => { + const create = mock(() => Promise.resolve({ id: 'pay_123' })); + const client = createTestClient({ + payments: { + create, + } as unknown as PaymeshClient['payments'], + }); + const tools = getRegisteredTools(client, resolveMcpConfig(client)); + const tool = tools.find((entry) => entry.name === 'payments_create'); + + await tool!.handler({ + data: { amount: 1000, currency: 'USD', productIds: ['prod_123'] }, + includeRaw: true, + }); + + expect(create).toHaveBeenCalledWith( + { amount: 1000, currency: 'USD', productIds: ['prod_123'] }, + { includeRaw: true, sandbox: undefined }, + ); + }); + + test('blocks requests above max list limit', async () => { + const client = createTestClient(); + const tools = getRegisteredTools( + client, + resolveMcpConfig(client, { maxListLimit: 5 }), + ); + const tool = tools.find((entry) => entry.name === 'customers_list'); + + expect(tool!.handler({ limit: 6 })).rejects.toThrow( + 'Requested list limit 6 exceeds the configured maxListLimit 5.', + ); + }); + + test('blocks live-mode servers unless allowLiveMode is enabled', () => { + const client = createTestClient({ + isSandbox: () => false, + }); + + expect(() => getRegisteredTools(client, resolveMcpConfig(client))).toThrow( + 'The Paymesh MCP server refuses to start in live mode unless allowLiveMode is enabled.', + ); + }); + + test('maps CLI options into MCP config overrides', () => { + const cliOptions = parseCliOptions({ + client: './client.mjs', + export: 'billing', + readonly: true, + maxListLimit: 10, + plugins: false, + }); + + expect(cliOptions).toEqual({ + client: './client.mjs', + export: 'billing', + enabled: undefined, + readonly: true, + includeRaw: undefined, + allowLiveMode: undefined, + maxListLimit: 10, + maxListLines: undefined, + tools: { + customers: undefined, + payments: undefined, + pix: undefined, + plugins: false, + }, + }); + }); + + test('returns structured JSON without serializing the full payload into text', () => { + const result = createToolResult({ + id: 'cus_123', + total: 1, + }); + + expect(result.structuredContent).toEqual({ + id: 'cus_123', + total: 1, + }); + expect(result.content).toEqual([ + { + type: 'text', + text: 'Structured JSON result.', + }, + ]); + }); +}); + +function createTestClient( + overrides: Partial> = {}, +): PaymeshClient { + const base = { + provider: { id: 'stub' }, + schema: { prefix: 'paymesh_', tables: {} }, + isSandbox: () => true, + $mcp: structuredClone(DEFAULT_MCP_CONFIG), + customers: { + list: async () => ({ data: [], total: 0, next: null, previous: null }), + get: async () => ({ id: 'cus_123' }), + getByEmail: async () => ({ id: 'cus_123' }), + getByExternalId: async () => ({ id: 'cus_123' }), + upsert: async (data: unknown) => data, + delete: async (id: string) => ({ id }), + }, + payments: { + create: async (data: unknown) => data, + }, + pix: { + create: async (data: unknown) => data, + get: async (id: string) => ({ id }), + }, + plugins: { + byId: {}, + list: () => [], + }, + webhooks: { + handle: async () => ({ status: 200, body: { received: true } }), + }, + routes: { + list: () => [], + handle: async () => new Response(null, { status: 404 }), + }, + capabilities: {}, + database: { + close: async () => {}, + repositories: { + customers: { + list: async () => ({ + data: [], + total: 0, + next: null, + previous: null, + }), + findByProviderId: async () => null, + upsert: async () => {}, + markDeleted: async () => {}, + findByEmail: async () => null, + findByExternalId: async () => null, + }, + payments: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + pix: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + invoices: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + checkouts: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + subscriptions: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + webhookEvents: { + acquire: async () => ({ duplicate: false }), + markProcessed: async () => {}, + markFailed: async () => {}, + }, + products: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + prices: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + paymentMethods: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + entitlements: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + usage: { + findByProviderId: async () => null, + upsert: async () => {}, + }, + migrations: { + list: async () => [], + apply: async () => {}, + }, + }, + }, + } as unknown as PaymeshClient; + + return { + ...base, + ...overrides, + }; +} + +async function createTempProject() { + const directory = await fs.mkdtemp(path.join(tmpdir(), 'paymesh-mcp-')); + tempDirectories.push(directory); + return directory; +} + +async function writeModule( + directory: string, + relativePath: string, + contents: string, +) { + await fs.writeFile(path.join(directory, relativePath), contents); +} diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 0000000..84a8064 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "paymesh": ["../paymesh/src/index.ts"], + }, + "types": ["bun"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/mcp/tsdown.config.mjs b/packages/mcp/tsdown.config.mjs new file mode 100644 index 0000000..72936b7 --- /dev/null +++ b/packages/mcp/tsdown.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['./src/index.ts', './src/cli.ts'], + format: ['esm', 'cjs'], + dts: true, + clean: true, + sourcemap: false, + minify: true, + target: 'node20', + outDir: 'dist', + external: ['paymesh', 'hono'], +}); diff --git a/packages/paymesh/src/client/managers.ts b/packages/paymesh/src/client/managers.ts index 1856bb9..195d424 100644 --- a/packages/paymesh/src/client/managers.ts +++ b/packages/paymesh/src/client/managers.ts @@ -35,9 +35,9 @@ export function createClientManagers< IncludeRaw extends boolean, Plugins extends readonly AnyPaymeshPlugin[], >({ - provider, - options, schema, + provider, + options: { mcp, ...options }, }: CreateClientManagersOptions): PaymeshClient< IncludeRaw, Schema, @@ -130,6 +130,19 @@ export function createClientManagers< list: () => [], }, capabilities: provider.capabilities, + $mcp: { + tools: { + pix: mcp?.tools?.pix ?? true, + payments: mcp?.tools?.payments ?? true, + customers: mcp?.tools?.customers ?? true, + plugins: mcp?.tools?.plugins ?? true, + }, + enabled: mcp?.enabled ?? true, + readonly: mcp?.readonly ?? false, + includeRaw: mcp?.includeRaw ?? false, + maxListLimit: mcp?.maxListLimit ?? 50, + allowLiveMode: mcp?.allowLiveMode ?? false, + }, } as PaymeshClient; const bootstrappedPlugins = bootstrapPlugins({ diff --git a/packages/paymesh/src/errors/index.ts b/packages/paymesh/src/errors/index.ts index 027cd34..a0c7731 100644 --- a/packages/paymesh/src/errors/index.ts +++ b/packages/paymesh/src/errors/index.ts @@ -16,7 +16,8 @@ export type PaymeshErrorCode = | 'network_error' | 'timeout' | 'client_error' - | 'cli_error'; + | 'cli_error' + | 'mcp_error'; /** * Structured input used to build a `PaymeshError`. diff --git a/packages/paymesh/src/index.ts b/packages/paymesh/src/index.ts index bee24ae..6b9ec82 100644 --- a/packages/paymesh/src/index.ts +++ b/packages/paymesh/src/index.ts @@ -63,13 +63,12 @@ export function createClient< if ( typeof options.sandbox === 'boolean' && options.sandbox !== provider.isSandbox() - ) { + ) throw new PaymeshError({ code: 'invalid_request', message: `Client sandbox option (${String(options.sandbox)}) does not match provider "${provider.id}" sandbox mode (${String(provider.isSandbox())}).`, provider: provider.id, }); - } const plugins = options.plugins ?? ([] as unknown as Plugins); const schema = resolveDatabaseSchema( diff --git a/packages/paymesh/src/types/client.ts b/packages/paymesh/src/types/client.ts index 289fcc0..c6daf0a 100644 --- a/packages/paymesh/src/types/client.ts +++ b/packages/paymesh/src/types/client.ts @@ -8,6 +8,7 @@ import type { PaymeshDatabaseDriver, ResolvedDatabaseSchema, } from './database'; +import type { PaymeshMCPMetadata, PaymeshMCPOptions } from './mcp'; import type { AnyPaymeshPlugin, LazyPluginExtension, @@ -369,6 +370,8 @@ export interface PaymeshClient< plugins: PaymeshPluginsClient; /** Provider capabilities exposed by the provider instance. */ capabilities: ProviderCapabilities; + /** Metadata used by the `@paymesh/mcp` package. */ + $mcp: PaymeshMCPMetadata; } /** Options accepted by `createClient`. */ @@ -400,4 +403,6 @@ export interface ClientOptions< hooks?: PaymeshHooks; /** Plugins to bootstrap alongside the client. */ plugins?: Plugins; + /** Options used by the `@paymesh/mcp` package. It's does not create the server, only metadata. */ + mcp?: PaymeshMCPOptions; } diff --git a/packages/paymesh/src/types/database.ts b/packages/paymesh/src/types/database.ts index 9e99ad1..576aebe 100644 --- a/packages/paymesh/src/types/database.ts +++ b/packages/paymesh/src/types/database.ts @@ -436,6 +436,28 @@ export interface PaymeshCustomersRepository { id: string, options?: PaymeshRepositoryReadOptions, ): Promise; + /** Finds a customer by email. */ + findByEmail?< + IncludeRaw extends boolean = false, + TCustomer extends Customer = Customer, + >( + schema: ResolvedDatabaseSchema, + provider: string, + sandbox: boolean, + email: string, + options?: PaymeshRepositoryReadOptions, + ): Promise; + /** Finds a customer by external id. */ + findByExternalId?< + IncludeRaw extends boolean = false, + TCustomer extends Customer = Customer, + >( + schema: ResolvedDatabaseSchema, + provider: string, + sandbox: boolean, + externalId: string, + options?: PaymeshRepositoryReadOptions, + ): Promise; /** Inserts or updates a customer row. */ upsert( schema: ResolvedDatabaseSchema, diff --git a/packages/paymesh/src/types/mcp.ts b/packages/paymesh/src/types/mcp.ts new file mode 100644 index 0000000..52e8e84 --- /dev/null +++ b/packages/paymesh/src/types/mcp.ts @@ -0,0 +1,17 @@ +export interface PaymeshMCPToolsOptions { + pix?: boolean; + customers?: boolean; + payments?: boolean; + plugins?: boolean; +} + +export interface PaymeshMCPOptions { + enabled?: boolean; + readonly?: boolean; + maxListLimit?: number; + includeRaw?: boolean; + allowLiveMode?: boolean; + tools?: PaymeshMCPToolsOptions; +} + +export type PaymeshMCPMetadata = Required; diff --git a/packages/postgres/src/repositories.ts b/packages/postgres/src/repositories.ts index bd56b61..0257aa2 100644 --- a/packages/postgres/src/repositories.ts +++ b/packages/postgres/src/repositories.ts @@ -74,6 +74,72 @@ export function createRepositories( options?.includeRaw, ) as never; }, + async findByEmail(schema, provider, sandbox, email, options) { + const fields = Object.values(schema.tables.customers.fields); + const [row] = await executor.query< + { + provider: string; + provider_id: string; + sandbox: boolean | null; + data: Record | null; + raw: unknown; + } & Record + >({ + sql: `SELECT provider, provider_id, sandbox, data, raw${fields.length === 0 ? '' : `, ${fields.map((field) => `${quoteIdentifier(field.column)} AS ${quoteIdentifier(field.key)}`).join(', ')}`} + FROM ${tableName(schema, 'customers')} + WHERE provider = $1 AND sandbox = $2 AND email = $3 AND deleted_at IS NULL + LIMIT 1`, + params: [provider, sandbox, email], + }); + + if (!row) return null; + + const data = hydrateStoredData(row.data, fields, row); + + return withRaw( + { + ...data, + id: row.provider_id, + provider: row.provider, + sandbox: row.sandbox ?? false, + }, + row.raw, + options?.includeRaw, + ) as never; + }, + async findByExternalId(schema, provider, sandbox, externalId, options) { + const fields = Object.values(schema.tables.customers.fields); + const [row] = await executor.query< + { + provider: string; + provider_id: string; + sandbox: boolean | null; + data: Record | null; + raw: unknown; + } & Record + >({ + sql: `SELECT provider, provider_id, sandbox, data, raw${fields.length === 0 ? '' : `, ${fields.map((field) => `${quoteIdentifier(field.column)} AS ${quoteIdentifier(field.key)}`).join(', ')}`} + FROM ${tableName(schema, 'customers')} + WHERE provider = $1 AND sandbox = $2 AND external_id = $3 AND deleted_at IS NULL + LIMIT 1`, + params: [provider, sandbox, externalId], + }); + + if (!row) return null; + + const data = hydrateStoredData(row.data, fields, row); + + return withRaw( + { + ...data, + id: row.provider_id, + provider: row.provider, + sandbox: row.sandbox ?? false, + }, + row.raw, + options?.includeRaw, + ) as never; + }, async list(schema, provider, sandbox, options) { const fields = Object.values(schema.tables.customers.fields); const limit = options?.limit ?? 20; diff --git a/packages/prisma/src/repositories.ts b/packages/prisma/src/repositories.ts index 6a4e387..af5828e 100644 --- a/packages/prisma/src/repositories.ts +++ b/packages/prisma/src/repositories.ts @@ -74,6 +74,72 @@ export function createRepositories( options?.includeRaw, ) as never; }, + async findByEmail(schema, provider, sandbox, email, options) { + const fields = Object.values(schema.tables.customers.fields); + const [row] = await executor.query< + { + provider: string; + provider_id: string; + sandbox: boolean | null; + data: Record | null; + raw: unknown; + } & Record + >({ + sql: `SELECT provider, provider_id, sandbox, data, raw${fields.length === 0 ? '' : `, ${fields.map((field) => `${quoteIdentifier(field.column)} AS ${quoteIdentifier(field.key)}`).join(', ')}`} + FROM ${tableName(schema, 'customers')} + WHERE provider = $1 AND sandbox = $2 AND email = $3 AND deleted_at IS NULL + LIMIT 1`, + params: [provider, sandbox, email], + }); + + if (!row) return null; + + const data = hydrateStoredData(row.data, fields, row); + + return withRaw( + { + ...data, + id: row.provider_id, + provider: row.provider, + sandbox: row.sandbox ?? false, + }, + row.raw, + options?.includeRaw, + ) as never; + }, + async findByExternalId(schema, provider, sandbox, externalId, options) { + const fields = Object.values(schema.tables.customers.fields); + const [row] = await executor.query< + { + provider: string; + provider_id: string; + sandbox: boolean | null; + data: Record | null; + raw: unknown; + } & Record + >({ + sql: `SELECT provider, provider_id, sandbox, data, raw${fields.length === 0 ? '' : `, ${fields.map((field) => `${quoteIdentifier(field.column)} AS ${quoteIdentifier(field.key)}`).join(', ')}`} + FROM ${tableName(schema, 'customers')} + WHERE provider = $1 AND sandbox = $2 AND external_id = $3 AND deleted_at IS NULL + LIMIT 1`, + params: [provider, sandbox, externalId], + }); + + if (!row) return null; + + const data = hydrateStoredData(row.data, fields, row); + + return withRaw( + { + ...data, + id: row.provider_id, + provider: row.provider, + sandbox: row.sandbox ?? false, + }, + row.raw, + options?.includeRaw, + ) as never; + }, async list(schema, provider, sandbox, options) { const fields = Object.values(schema.tables.customers.fields); const limit = options?.limit ?? 20;