Skip to content

Commit d8d1d50

Browse files
npslaneyclaude
andcommitted
feat(api-contract): add MCP contracts for CRUD operations
Add separate mcpContract namespace for MCP tools: - customers: list, get, create, update, delete - products: list, get, create, update, delete - orders: list, get (read-only) - checkouts: list, get (read-only) Export mcpContract from index.ts alongside SDK contract. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b1b9683 commit d8d1d50

6 files changed

Lines changed: 273 additions & 0 deletions

File tree

src/contracts/mcp/checkouts.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { oc } from "@orpc/contract";
2+
import { z } from "zod";
3+
import {
4+
PaginationInputSchema,
5+
PaginationOutputSchema,
6+
} from "../../schemas/pagination";
7+
import { CustomerSchema } from "../../schemas/customer";
8+
import { CurrencySchema } from "../../schemas/currency";
9+
10+
/**
11+
* Checkout status enum.
12+
* NOTE: This is "checkouts.*" namespace for read APIs (list/get),
13+
* distinct from "checkout.*" namespace which handles the SDK create/confirm flow.
14+
*/
15+
const CheckoutStatusSchema = z.enum([
16+
"UNCONFIRMED",
17+
"CONFIRMED",
18+
"PENDING_PAYMENT",
19+
"PAYMENT_RECEIVED",
20+
"EXPIRED",
21+
]);
22+
23+
// Simplified checkout schema for MCP listing
24+
// Note: Uses modifiedAt to match Prisma schema naming
25+
const CheckoutListItemSchema = z.object({
26+
id: z.string(),
27+
status: CheckoutStatusSchema,
28+
type: z.enum(["PRODUCTS", "AMOUNT", "TOP_UP"]),
29+
currency: CurrencySchema,
30+
totalAmount: z.number().nullable(),
31+
customerId: z.string().nullable(),
32+
customer: CustomerSchema.nullable(),
33+
productId: z.string().nullable(),
34+
organizationId: z.string(),
35+
expiresAt: z.date(),
36+
createdAt: z.date(),
37+
modifiedAt: z.date().nullable(),
38+
});
39+
40+
// Full checkout detail schema
41+
const CheckoutDetailSchema = CheckoutListItemSchema.extend({
42+
userMetadata: z.record(z.string(), z.any()).nullable(),
43+
successUrl: z.string().nullable(),
44+
discountAmount: z.number().nullable(),
45+
netAmount: z.number().nullable(),
46+
taxAmount: z.number().nullable(),
47+
});
48+
49+
const ListCheckoutsInputSchema = PaginationInputSchema.extend({
50+
status: CheckoutStatusSchema.optional(),
51+
});
52+
53+
const ListCheckoutsOutputSchema = PaginationOutputSchema.extend({
54+
checkouts: z.array(CheckoutListItemSchema),
55+
});
56+
57+
export const listCheckoutsContract = oc
58+
.input(ListCheckoutsInputSchema)
59+
.output(ListCheckoutsOutputSchema);
60+
61+
export const getCheckoutContract = oc
62+
.input(z.object({ id: z.string() }))
63+
.output(CheckoutDetailSchema);
64+
65+
export const checkouts = {
66+
list: listCheckoutsContract,
67+
get: getCheckoutContract,
68+
};

src/contracts/mcp/customers.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { oc } from "@orpc/contract";
2+
import { z } from "zod";
3+
import { CustomerSchema } from "../../schemas/customer";
4+
import {
5+
PaginationInputSchema,
6+
PaginationOutputSchema,
7+
} from "../../schemas/pagination";
8+
9+
const ListCustomersInputSchema = PaginationInputSchema;
10+
const ListCustomersOutputSchema = PaginationOutputSchema.extend({
11+
customers: z.array(CustomerSchema),
12+
});
13+
14+
const GetCustomerInputSchema = z.object({ id: z.string() });
15+
16+
const CreateCustomerInputSchema = z.object({
17+
name: z.string().min(1),
18+
email: z.string().email(),
19+
});
20+
21+
const UpdateCustomerInputSchema = z.object({
22+
id: z.string(),
23+
name: z.string().optional(),
24+
email: z.string().email().optional(),
25+
userMetadata: z.record(z.string(), z.string()).optional(),
26+
});
27+
28+
const DeleteCustomerInputSchema = z.object({ id: z.string() });
29+
30+
export const listCustomersContract = oc
31+
.input(ListCustomersInputSchema)
32+
.output(ListCustomersOutputSchema);
33+
34+
export const getCustomerContract = oc
35+
.input(GetCustomerInputSchema)
36+
.output(CustomerSchema);
37+
38+
export const createCustomerContract = oc
39+
.input(CreateCustomerInputSchema)
40+
.output(CustomerSchema);
41+
42+
export const updateCustomerContract = oc
43+
.input(UpdateCustomerInputSchema)
44+
.output(CustomerSchema);
45+
46+
export const deleteCustomerContract = oc
47+
.input(DeleteCustomerInputSchema)
48+
.output(z.object({ ok: z.literal(true) }));
49+
50+
export const customers = {
51+
list: listCustomersContract,
52+
get: getCustomerContract,
53+
create: createCustomerContract,
54+
update: updateCustomerContract,
55+
delete: deleteCustomerContract,
56+
};

src/contracts/mcp/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* MCP Contract namespace - separate from SDK contract.
3+
*
4+
* This contract is used by MCP tools for organization management.
5+
* It uses OAuth authentication (not API key auth) and provides
6+
* CRUD operations for customers, products, orders, and checkouts.
7+
*
8+
* NOTE: This is deliberately NOT exported to the SDK. It's only
9+
* consumed by the MCP server via the dedicated /rpc/mcp endpoint.
10+
*/
11+
12+
export { customers } from "./customers";
13+
export { products } from "./products";
14+
export { orders } from "./orders";
15+
export { checkouts } from "./checkouts";

src/contracts/mcp/orders.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { oc } from "@orpc/contract";
2+
import { z } from "zod";
3+
import { OrderSchema, OrderItemSchema } from "../../schemas/order";
4+
import {
5+
PaginationInputSchema,
6+
PaginationOutputSchema,
7+
} from "../../schemas/pagination";
8+
import { CustomerSchema } from "../../schemas/customer";
9+
10+
// Order with related data for list view
11+
const OrderWithRelationsSchema = OrderSchema.extend({
12+
customer: CustomerSchema.nullable(),
13+
orderItems: z.array(OrderItemSchema),
14+
});
15+
16+
// Order with full details for get view
17+
const OrderDetailSchema = OrderWithRelationsSchema;
18+
19+
const ListOrdersInputSchema = PaginationInputSchema.extend({
20+
customerId: z.string().optional(),
21+
status: z.string().optional(), // Prisma uses String type for status
22+
});
23+
24+
const ListOrdersOutputSchema = PaginationOutputSchema.extend({
25+
orders: z.array(OrderWithRelationsSchema),
26+
});
27+
28+
export const listOrdersContract = oc
29+
.input(ListOrdersInputSchema)
30+
.output(ListOrdersOutputSchema);
31+
32+
export const getOrderContract = oc
33+
.input(z.object({ id: z.string() }))
34+
.output(OrderDetailSchema);
35+
36+
export const orders = {
37+
list: listOrdersContract,
38+
get: getOrderContract,
39+
};

src/contracts/mcp/products.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { oc } from "@orpc/contract";
2+
import { z } from "zod";
3+
import { ProductSchema, ProductPriceSchema } from "../products";
4+
import {
5+
PaginationInputSchema,
6+
PaginationOutputSchema,
7+
} from "../../schemas/pagination";
8+
import { ProductPriceInputSchema } from "../../schemas/product-price-input";
9+
10+
// Output schema - product with its active price
11+
// Note: Uses modifiedAt to match Prisma schema naming
12+
const ProductWithPriceSchema = ProductSchema.omit({ prices: true }).extend({
13+
price: ProductPriceSchema.nullable(),
14+
userMetadata: z.record(z.string(), z.any()).nullable(),
15+
createdAt: z.date(),
16+
modifiedAt: z.date().nullable(),
17+
});
18+
19+
const ListProductsOutputSchema = PaginationOutputSchema.extend({
20+
products: z.array(ProductWithPriceSchema),
21+
});
22+
23+
// Create input - NO benefitIds (not exposed on dashboard MCP)
24+
const CreateProductInputSchema = z.object({
25+
name: z.string().min(1),
26+
description: z.string().optional(),
27+
price: ProductPriceInputSchema,
28+
userMetadata: z.record(z.string(), z.string()).optional(),
29+
});
30+
31+
// Update input - includes price (matches dashboard updateProduct), NO benefitIds
32+
const UpdateProductInputSchema = z.object({
33+
id: z.string(),
34+
name: z.string().min(1).optional(),
35+
description: z.string().optional(),
36+
price: ProductPriceInputSchema.optional(), // Can update pricing (immutable pattern applies)
37+
userMetadata: z.record(z.string(), z.string()).optional(),
38+
});
39+
40+
export const listProductsContract = oc
41+
.input(PaginationInputSchema)
42+
.output(ListProductsOutputSchema);
43+
44+
export const getProductContract = oc
45+
.input(z.object({ id: z.string() }))
46+
.output(ProductWithPriceSchema);
47+
48+
export const createProductContract = oc
49+
.input(CreateProductInputSchema)
50+
.output(ProductWithPriceSchema);
51+
52+
export const updateProductContract = oc
53+
.input(UpdateProductInputSchema)
54+
.output(ProductWithPriceSchema);
55+
56+
export const deleteProductContract = oc
57+
.input(z.object({ id: z.string() }))
58+
.output(z.object({ ok: z.literal(true) }));
59+
60+
export const products = {
61+
list: listProductsContract,
62+
get: getProductContract,
63+
create: createProductContract,
64+
update: updateProductContract,
65+
delete: deleteProductContract,
66+
};

src/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { checkout } from "./contracts/checkout";
22
import { onboarding } from "./contracts/onboarding";
33
import { products } from "./contracts/products";
4+
import {
5+
customers as mcpCustomers,
6+
products as mcpProducts,
7+
orders as mcpOrders,
8+
checkouts as mcpCheckouts,
9+
} from "./contracts/mcp";
410

511
export type {
612
ConfirmCheckout,
@@ -28,8 +34,31 @@ export {
2834
ListProductsOutputSchema,
2935
} from "./contracts/products";
3036

37+
// New MCP schemas
38+
export type { Customer } from "./schemas/customer";
39+
export { CustomerSchema } from "./schemas/customer";
40+
export type { Order, OrderItem } from "./schemas/order";
41+
export { OrderSchema, OrderItemSchema } from "./schemas/order";
42+
export type { PaginationInput, PaginationOutput } from "./schemas/pagination";
43+
export { PaginationInputSchema, PaginationOutputSchema } from "./schemas/pagination";
44+
export type { ProductPriceInput } from "./schemas/product-price-input";
45+
export { ProductPriceInputSchema } from "./schemas/product-price-input";
46+
47+
// SDK contract - consumed by SDK clients
3148
export const contract = { checkout, onboarding, products };
3249

50+
/**
51+
* MCP contract - separate namespace for MCP tools.
52+
* NOT consumed by SDK, only by MCP server via /rpc/mcp endpoint.
53+
* Uses OAuth authentication (not API key auth).
54+
*/
55+
export const mcpContract = {
56+
customers: mcpCustomers,
57+
products: mcpProducts,
58+
orders: mcpOrders,
59+
checkouts: mcpCheckouts,
60+
};
61+
3362
export type { MetadataValidationError } from "./validation/metadata-validation";
3463
export {
3564
MAX_KEY_COUNT,

0 commit comments

Comments
 (0)