This commit is contained in:
Shotaro Nakamura
2026-06-19 21:39:59 +09:00
parent 5a15172513
commit b843a5ff38
36 changed files with 3891 additions and 1008 deletions
+24
View File
@@ -12,6 +12,18 @@ export type ServerConfig = {
authBaseUrl?: string
mtsBaseUrl?: string
izanagiBaseUrl?: string
foreignStockBaseUrl?: string
usStockBaseUrl?: string
foreignStockRestUrl?: string
foreignStockGraphqlBffUrl?: string
foreignStockGraphqlIntUrl?: string
mainSiteBaseUrl?: string
mainSiteEtGatePath?: string
mainSiteAssetsValuationsPath?: string
mainSiteExchangeOrderInputPath?: string
mainSiteExchangeOrderPasswordPath?: string
mainSiteExchangeOrderConfirmPath?: string
mainSiteExchangeOrderCompletePath?: string
}
const optionalUrl = (value: string | undefined) => {
@@ -38,5 +50,17 @@ export const loadConfig = (): ServerConfig => {
authBaseUrl: optionalUrl(process.env.SBI_AUTH_BASE_URL),
mtsBaseUrl: optionalUrl(process.env.SBI_MTS_BASE_URL),
izanagiBaseUrl: optionalUrl(process.env.SBI_IZANAGI_BASE_URL),
foreignStockBaseUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_BASE_URL),
usStockBaseUrl: optionalUrl(process.env.SBI_US_STOCK_BASE_URL),
foreignStockRestUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_REST_URL),
foreignStockGraphqlBffUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_GRAPHQL_BFF_URL),
foreignStockGraphqlIntUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_GRAPHQL_INT_URL),
mainSiteBaseUrl: optionalUrl(process.env.SBI_MAIN_SITE_BASE_URL),
mainSiteEtGatePath: process.env.SBI_MAIN_SITE_ET_GATE_PATH,
mainSiteAssetsValuationsPath: process.env.SBI_MAIN_SITE_ASSETS_VALUATIONS_PATH,
mainSiteExchangeOrderInputPath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_INPUT_PATH,
mainSiteExchangeOrderPasswordPath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_PASSWORD_PATH,
mainSiteExchangeOrderConfirmPath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_CONFIRM_PATH,
mainSiteExchangeOrderCompletePath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_COMPLETE_PATH,
}
}
+34
View File
@@ -75,6 +75,7 @@ const orderSubmitMethodByEstimateMethod = {
'orders.ifd.estimateCorrection': 'orders.ifd.placeCorrection',
'orders.ifd.estimateCancel': 'orders.ifd.placeCancel',
'orders.themeInvestment.estimate': 'orders.themeInvestment.place',
'orders.exchange.estimate': 'orders.exchange.place',
} as const satisfies Partial<Record<RpcMethod, RpcMethod>>
const submitMethodForEstimateMethod = (method: RpcMethod) =>
@@ -567,9 +568,39 @@ const placeThemeInvestmentOrderSchema = themeInvestmentOrderSchema.extend({
.describe('Explicitly allows sending a live theme investment order'),
})
const exchangeOrderSchema = z.object({
currencyCode: z.string().min(1).describe('Currency code, such as USD'),
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'),
specificMethod: z
.enum(['foreign', 'domestic'])
.optional()
.describe('foreign for foreign-currency quantity, domestic for yen amount'),
accountKind: z.enum(['GENERAL', 'JR_NISA']).optional().describe('SBI exchange account kind'),
sellMethod: z.enum(['SELL_PART', 'SELL_ALL']).optional().describe('Required for sell orders'),
orderAmount: z
.union([z.number().positive(), z.string().min(1)])
.optional()
.describe('Hidden order amount posted to SBI; required for domestic orders'),
tradePassword: z.string().optional().describe('Trading password used by SBI'),
})
const exchangeRateSchema = exchangeOrderSchema.pick({
currencyCode: true,
side: true,
})
const placeExchangeOrderSchema = exchangeOrderSchema.extend({
allowTrading: z
.literal(true)
.optional()
.describe('Explicitly allows sending a live exchange order'),
})
const methodParamSchemas = {
'session.profile': undefined,
'account.profile': undefined,
'account.assets.current': undefined,
'account.power.buyingPower': undefined,
'account.power.collateralRatio': undefined,
'account.positions.cash': cashPositionOptionsSchema.optional(),
@@ -629,6 +660,9 @@ const methodParamSchemas = {
'orders.themeInvestment.list': themeInvestmentPreOrderSchema,
'orders.themeInvestment.estimate': themeInvestmentOrderSchema,
'orders.themeInvestment.place': placeThemeInvestmentOrderSchema,
'orders.exchange.rate': exchangeRateSchema,
'orders.exchange.estimate': exchangeOrderSchema,
'orders.exchange.place': placeExchangeOrderSchema,
} satisfies Record<RpcMethod, z.ZodType | undefined>
const createMcpServer = (c: Context<AppBindings>) => {
+5
View File
@@ -3,6 +3,7 @@ import type { SbiClientMethods } from '@repo/sbi-client'
export const RPC_METHODS = [
'session.profile',
'account.profile',
'account.assets.current',
'account.power.buyingPower',
'account.power.collateralRatio',
'account.positions.cash',
@@ -62,6 +63,9 @@ export const RPC_METHODS = [
'orders.themeInvestment.list',
'orders.themeInvestment.estimate',
'orders.themeInvestment.place',
'orders.exchange.rate',
'orders.exchange.estimate',
'orders.exchange.place',
] as const
export type RpcMethod = (typeof RPC_METHODS)[number]
@@ -80,6 +84,7 @@ const tradingMethods = new Set<string>([
'orders.ifd.placeCorrection',
'orders.ifd.placeCancel',
'orders.themeInvestment.place',
'orders.exchange.place',
])
const cashOrderMethods = new Set<string>([
+12
View File
@@ -27,6 +27,18 @@ export const connectSbi = async (
authBaseUrl: config.authBaseUrl,
mtsBaseUrl: config.mtsBaseUrl,
izanagiBaseUrl: config.izanagiBaseUrl,
foreignStockBaseUrl: config.foreignStockBaseUrl,
usStockBaseUrl: config.usStockBaseUrl,
foreignStockRestUrl: config.foreignStockRestUrl,
foreignStockGraphqlBffUrl: config.foreignStockGraphqlBffUrl,
foreignStockGraphqlIntUrl: config.foreignStockGraphqlIntUrl,
mainSiteBaseUrl: config.mainSiteBaseUrl,
mainSiteEtGatePath: config.mainSiteEtGatePath,
mainSiteAssetsValuationsPath: config.mainSiteAssetsValuationsPath,
mainSiteExchangeOrderInputPath: config.mainSiteExchangeOrderInputPath,
mainSiteExchangeOrderPasswordPath: config.mainSiteExchangeOrderPasswordPath,
mainSiteExchangeOrderConfirmPath: config.mainSiteExchangeOrderConfirmPath,
mainSiteExchangeOrderCompletePath: config.mainSiteExchangeOrderCompletePath,
},
clientOptions,
)
+8 -5
View File
@@ -1,4 +1,4 @@
import type { SbiClientMethods } from '@repo/sbi-client'
import type { MarketCode, SbiClientMethods } from '@repo/sbi-client'
import { createBunWebSocket } from 'hono/bun'
import type { WSContext } from 'hono/ws'
import { randomUUID } from 'node:crypto'
@@ -27,7 +27,7 @@ type RpcSocketState = {
type BoardPollingParams = {
issueCode: string
market?: string
market: MarketCode
intervalSeconds?: number
}
@@ -67,8 +67,11 @@ const parseBoardPollingParams = (params: unknown): BoardPollingParams => {
if (typeof value.issueCode !== 'string' || !value.issueCode) {
throw new Error('issueCode is required')
}
if (value.market != null && typeof value.market !== 'string') {
throw new Error('market must be a string')
if (typeof value.market !== 'string' || !value.market) {
throw new Error('market is required')
}
if (!['XTKS', 'XNAS', 'XNYS', 'ARCX'].includes(value.market)) {
throw new Error('market must be XTKS, XNAS, XNYS, or ARCX')
}
if (
value.intervalSeconds != null &&
@@ -81,7 +84,7 @@ const parseBoardPollingParams = (params: unknown): BoardPollingParams => {
return {
issueCode: value.issueCode,
market: typeof value.market === 'string' ? value.market : undefined,
market: value.market as MarketCode,
intervalSeconds: typeof value.intervalSeconds === 'number' ? value.intervalSeconds : undefined,
}
}