This commit is contained in:
Shotaro Nakamura
2026-06-21 17:51:50 +09:00
parent 207f9c2164
commit 614ea8a992
+115 -36
View File
@@ -194,7 +194,8 @@ const boardOptionsSchema = issueOptionsSchema.extend({
.optional(), .optional(),
}) })
const stockOrderBaseSchema = z.object({ const stockOrderBaseSchema = z
.object({
issueCode: issueCodeSchema, issueCode: issueCodeSchema,
market: marketCodeSchema, market: marketCodeSchema,
side: tradeSideSchema, side: tradeSideSchema,
@@ -202,6 +203,7 @@ const stockOrderBaseSchema = z.object({
quantity: z.number().positive().describe('Order quantity'), quantity: z.number().positive().describe('Order quantity'),
depositType: depositTypeSchema.optional(), depositType: depositTypeSchema.optional(),
}) })
.strict()
const cashOrderPriceConditionSchema = z.enum([ const cashOrderPriceConditionSchema = z.enum([
'limit', 'limit',
@@ -218,9 +220,14 @@ const cashOrderPriceConditionSchema = z.enum([
const cashOrderSchema = stockOrderBaseSchema.extend({ const cashOrderSchema = stockOrderBaseSchema.extend({
preOrderMarket: marketCodeSchema preOrderMarket: marketCodeSchema
.optional() .optional()
.describe('APK pre-order market for S-kabu; live S-kabu orders still send STK'), .describe(
'Original listed market for S-kabu checks, such as XTKS. For S-kabu live orders set kind: "s", market: "STK", and preOrderMarket to the listed market.',
),
price: z.number().positive().optional().describe('Order price for price-based orders'), price: z.number().positive().optional().describe('Order price for price-based orders'),
kind: z.enum(['market', 'limit', 'stop', 'oco', 'ifd', 'ifdo', 's', 'unknown']).optional(), kind: z
.enum(['market', 'limit', 'stop', 'oco', 'ifd', 'ifdo', 's', 'unknown'])
.optional()
.describe('Use "s" for S-kabu fractional-share cash orders. Do not use sStock.'),
priceCondition: cashOrderPriceConditionSchema priceCondition: cashOrderPriceConditionSchema
.optional() .optional()
.describe('APK/MTS execution condition for cash orders'), .describe('APK/MTS execution condition for cash orders'),
@@ -245,20 +252,30 @@ const cashOrderSchema = stockOrderBaseSchema.extend({
.describe('Previous market code sent with SOR orders; defaults to login profile'), .describe('Previous market code sent with SOR orders; defaults to login profile'),
}) })
const cashOrderPreOrderSchema = z.object({ const cashOrderPreOrderSchema = z
.object({
issueCode: issueCodeSchema, issueCode: issueCodeSchema,
market: marketCodeSchema, market: marketCodeSchema,
side: tradeSideSchema, side: tradeSideSchema,
accountType: accountTypeSchema.optional(), accountType: accountTypeSchema.optional(),
depositType: depositTypeSchema.optional(), depositType: depositTypeSchema.optional(),
kind: z.enum(['s']).optional().describe('Requests APK S-kabu pre-order constraints'), kind: z
preOrderMarket: marketCodeSchema.optional().describe('APK pre-order market for S-kabu checks'), .enum(['s'])
.optional()
.describe('Set to "s" to request APK S-kabu pre-order constraints. Do not use sStock.'),
preOrderMarket: marketCodeSchema
.optional()
.describe('Original listed market for S-kabu preflight, such as XTKS.'),
}) })
.strict()
const stockOrderMarginPositionSchema = z.object({ const stockOrderMarginPositionSchema = z
.object({
openTradeDate: z openTradeDate: z
.string() .string()
.describe('Open trade date from the margin position record, in yyyyMMdd or yyyy-MM-dd format'), .describe(
'Open trade date from the margin position record, in yyyyMMdd or yyyy-MM-dd format',
),
openPrice: z openPrice: z
.union([z.number().positive(), z.string().min(1)]) .union([z.number().positive(), z.string().min(1)])
.describe('Open price from the margin position record'), .describe('Open price from the margin position record'),
@@ -270,6 +287,7 @@ const stockOrderMarginPositionSchema = z.object({
'Bargain market code from the margin position record', 'Bargain market code from the margin position record',
), ),
}) })
.strict()
const placeCashOrderSchema = cashOrderSchema.extend({ const placeCashOrderSchema = cashOrderSchema.extend({
confirmationId: z confirmationId: z
@@ -283,7 +301,8 @@ const placeCashOrderSchema = cashOrderSchema.extend({
allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live order'), allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live order'),
}) })
const orderCorrectionSchema = z.object({ const orderCorrectionSchema = z
.object({
orderNumber: z orderNumber: z
.string() .string()
.min(1) .min(1)
@@ -325,6 +344,7 @@ const orderCorrectionSchema = z.object({
quantity: z.number().positive().optional().describe('Corrected order quantity'), quantity: z.number().positive().optional().describe('Corrected order quantity'),
price: z.number().positive().optional().describe('Corrected order price'), price: z.number().positive().optional().describe('Corrected order price'),
}) })
.strict()
const placeOrderCorrectionSchema = orderCorrectionSchema.extend({ const placeOrderCorrectionSchema = orderCorrectionSchema.extend({
allowTrading: z allowTrading: z
@@ -333,7 +353,8 @@ const placeOrderCorrectionSchema = orderCorrectionSchema.extend({
.describe('Explicitly allows sending a live correction request'), .describe('Explicitly allows sending a live correction request'),
}) })
const orderCancelSchema = z.object({ const orderCancelSchema = z
.object({
orderNumber: z.string().min(1).describe('Order number shown in order inquiry'), orderNumber: z.string().min(1).describe('Order number shown in order inquiry'),
orderId: orderIdSchema.optional().describe('Original order id shown in order inquiry'), orderId: orderIdSchema.optional().describe('Original order id shown in order inquiry'),
issueCode: issueCodeSchema.optional().describe('Original issue code shown in order inquiry'), issueCode: issueCodeSchema.optional().describe('Original issue code shown in order inquiry'),
@@ -341,6 +362,7 @@ const orderCancelSchema = z.object({
tradeId: z.string().min(1).optional().describe('Original trade id code'), tradeId: z.string().min(1).optional().describe('Original trade id code'),
cancelType: z.string().min(1).optional().describe('Additional cancel flag'), cancelType: z.string().min(1).optional().describe('Additional cancel flag'),
}) })
.strict()
const placeOrderCancelSchema = orderCancelSchema.extend({ const placeOrderCancelSchema = orderCancelSchema.extend({
tradePassword: z.string().optional().describe('Trading password used by SBI'), tradePassword: z.string().optional().describe('Trading password used by SBI'),
@@ -423,7 +445,8 @@ const placeMarginCloseOrderSchema = marginCloseOrderSchema.extend({
.describe('Explicitly allows sending a live margin close order'), .describe('Explicitly allows sending a live margin close order'),
}) })
const actualDeliveryOrderSchema = z.object({ const actualDeliveryOrderSchema = z
.object({
issueCode: issueCodeSchema, issueCode: issueCodeSchema,
market: marketCodeSchema, market: marketCodeSchema,
accountType: accountTypeSchema.optional(), accountType: accountTypeSchema.optional(),
@@ -441,6 +464,7 @@ const actualDeliveryOrderSchema = z.object({
.optional() .optional()
.describe('APK ippan margin payment-limit code from board/pre-order information'), .describe('APK ippan margin payment-limit code from board/pre-order information'),
}) })
.strict()
const actualDeliveryOrderPreOrderSchema = actualDeliveryOrderSchema.omit({ const actualDeliveryOrderPreOrderSchema = actualDeliveryOrderSchema.omit({
quantity: true, quantity: true,
@@ -517,7 +541,8 @@ const placeIfdOrderSchema = ifdOrderSchema.extend({
allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live IFD order'), allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live IFD order'),
}) })
const themeInvestmentOrderSchema = z.object({ const themeInvestmentOrderSchema = z
.object({
themeId: z.string().min(1).describe('Theme ID for the theme investment order'), themeId: z.string().min(1).describe('Theme ID for the theme investment order'),
themeSetYyyymm: z themeSetYyyymm: z
.string() .string()
@@ -539,10 +564,16 @@ const themeInvestmentOrderSchema = z.object({
.min(1) .min(1)
.max(10) .max(10)
.describe('Component stock orders selected by the mobile theme investment flow'), .describe('Component stock orders selected by the mobile theme investment flow'),
amount: z.number().positive().optional().describe('Order amount for the theme investment order'), amount: z
.number()
.positive()
.optional()
.describe('Order amount for the theme investment order'),
}) })
.strict()
const themeInvestmentPreOrderSchema = z.object({ const themeInvestmentPreOrderSchema = z
.object({
themeId: z.string().min(1).describe('Theme ID from the mobile theme investment handoff'), themeId: z.string().min(1).describe('Theme ID from the mobile theme investment handoff'),
themeName: z themeName: z
.string() .string()
@@ -561,6 +592,7 @@ const themeInvestmentPreOrderSchema = z.object({
.max(10) .max(10)
.describe('Component stocks selected by the mobile theme investment flow'), .describe('Component stocks selected by the mobile theme investment flow'),
}) })
.strict()
const placeThemeInvestmentOrderSchema = themeInvestmentOrderSchema.extend({ const placeThemeInvestmentOrderSchema = themeInvestmentOrderSchema.extend({
allowTrading: z allowTrading: z
@@ -569,7 +601,8 @@ const placeThemeInvestmentOrderSchema = themeInvestmentOrderSchema.extend({
.describe('Explicitly allows sending a live theme investment order'), .describe('Explicitly allows sending a live theme investment order'),
}) })
const exchangeOrderSchema = z.object({ const exchangeOrderSchema = z
.object({
currencyCode: z.string().min(1).describe('Currency code, such as USD'), currencyCode: z.string().min(1).describe('Currency code, such as USD'),
side: z.enum(['buy', 'sell']).describe('Buy or sell the foreign currency'), side: z.enum(['buy', 'sell']).describe('Buy or sell the foreign currency'),
tradeQuantity: z.union([z.number().positive(), z.string().min(1)]).describe('Order quantity'), tradeQuantity: z.union([z.number().positive(), z.string().min(1)]).describe('Order quantity'),
@@ -585,6 +618,7 @@ const exchangeOrderSchema = z.object({
.describe('Hidden order amount posted to SBI; required for domestic orders'), .describe('Hidden order amount posted to SBI; required for domestic orders'),
tradePassword: z.string().optional().describe('Trading password used by SBI'), tradePassword: z.string().optional().describe('Trading password used by SBI'),
}) })
.strict()
const exchangeRateSchema = exchangeOrderSchema.pick({ const exchangeRateSchema = exchangeOrderSchema.pick({
currencyCode: true, currencyCode: true,
@@ -732,8 +766,57 @@ const getActions = Object.keys(getActionToMethod) as Array<keyof typeof getActio
const changeActions = Object.keys(changeActionToEstimateMethod) as Array< const changeActions = Object.keys(changeActionToEstimateMethod) as Array<
keyof typeof changeActionToEstimateMethod keyof typeof changeActionToEstimateMethod
> >
const getActionSchema = z.enum(['capabilities', 'passkeys', ...getActions]) const getActionDescription = `Available actions: capabilities, passkeys, ${getActions
const changeActionSchema = z.enum(changeActions) .map((action) => `${action}=${getActionToMethod[action]}`)
.join(
', ',
)}. S-kabu preflight uses cashOrderPreflight with input.kind: "s" and input.preOrderMarket, not sStock.`
const changeActionDescription = `Available actions: ${changeActions
.map((action) => `${action}=${changeActionToEstimateMethod[action]}`)
.join(
', ',
)}. S-kabu cash orders use cashOrder with input.kind: "s", input.market: "STK", and input.preOrderMarket set to the listed market such as XTKS; do not use sStock, orderType, or sor.`
const getToolInputSchema = z.discriminatedUnion('action', [
z.object({ action: z.literal('capabilities') }).strict(),
z.object({ action: z.literal('passkeys') }).strict(),
...getActions.map((action) => {
const method = getActionToMethod[action]
const paramsSchema = methodParamSchemas[method]
return z
.object({
action: z.literal(action),
passkeyId: z.string().describe('Saved SBI passkey id'),
...(paramsSchema ? { input: paramsSchema.describe(`${method} input`) } : {}),
})
.strict()
}),
] as any)
const changeToolInputSchema = z.discriminatedUnion(
'action',
changeActions.map((action) => {
const method = changeActionToEstimateMethod[action]
const paramsSchema = methodParamSchemas[method]
if (!paramsSchema) throw new Error(`${method} must have an input schema`)
return z
.object({
action: z.literal(action),
passkeyId: z.string().describe('Saved SBI passkey id'),
input: paramsSchema.describe(`${method} input`),
})
.strict()
}) as any,
)
type GetToolArgs =
| { action: 'capabilities' }
| { action: 'passkeys' }
| {
action: keyof typeof getActionToMethod
passkeyId: string
input?: unknown
}
const parseMethodParams = (method: RpcMethod, input: unknown) => { const parseMethodParams = (method: RpcMethod, input: unknown) => {
const schema = methodParamSchemas[method] const schema = methodParamSchemas[method]
@@ -863,18 +946,12 @@ const createMcpServer = (c: Context<AppBindings>) => {
'csbie-get', 'csbie-get',
{ {
title: 'Get SBI Data', title: 'Get SBI Data',
description: description: `Read SBI data through a small abstract action API. This tool never places, corrects, cancels, or otherwise changes real orders. ${getActionDescription}`,
'Read SBI data through a small abstract action API. This tool never places, corrects, cancels, or otherwise changes real orders.', inputSchema: getToolInputSchema,
inputSchema: {
action: getActionSchema.describe('Read action to perform'),
passkeyId: z
.string()
.optional()
.describe('Saved SBI passkey id. Required for SBI-backed read actions.'),
input: z.unknown().optional().describe('Action input object'),
}, },
}, async (args) => {
async ({ action, passkeyId, input }) => { const parsedArgs = getToolInputSchema.parse(args) as GetToolArgs
const { action } = parsedArgs
requireAuthenticated(auth) requireAuthenticated(auth)
if (action === 'capabilities') { if (action === 'capabilities') {
@@ -890,6 +967,7 @@ const createMcpServer = (c: Context<AppBindings>) => {
const method = getActionToMethod[action] const method = getActionToMethod[action]
if (!method) throw new Error(`unsupported get action: ${String(action)}`) if (!method) throw new Error(`unsupported get action: ${String(action)}`)
const { passkeyId, input } = parsedArgs
if (!passkeyId) throw new Error(`${action} requires passkeyId`) if (!passkeyId) throw new Error(`${action} requires passkeyId`)
const params = parseMethodParams(method, input) const params = parseMethodParams(method, input)
@@ -901,16 +979,17 @@ const createMcpServer = (c: Context<AppBindings>) => {
'csbie-request-change', 'csbie-request-change',
{ {
title: 'Create SBI Change Request', title: 'Create SBI Change Request',
description: description: `Prepare a real SBI change by running the corresponding estimate/preview and returning a UUID. This tool never submits the change; pass the UUID to confirm-request. ${changeActionDescription}`,
'Prepare a real SBI change by running the corresponding estimate/preview and returning a UUID. This tool never submits the change; pass the UUID to confirm-request.', inputSchema: changeToolInputSchema,
inputSchema: {
action: changeActionSchema.describe('Change action to prepare'),
passkeyId: z.string().describe('Saved SBI passkey id'),
input: z.unknown().describe('Change action input object'),
}, },
async (args) => {
const { action, passkeyId, input } = changeToolInputSchema.parse(args) as {
action: keyof typeof changeActionToEstimateMethod
passkeyId: string
input: unknown
}
return textResult(await createChangeRequest(action, passkeyId, input))
}, },
async ({ action, passkeyId, input }) =>
textResult(await createChangeRequest(action, passkeyId, input)),
) )
server.registerTool( server.registerTool(