Game APIとInventory MCPサーバーの実装
タスク1: ゲームAPIの実装
Section titled “タスク1: ゲームAPIの実装”ゲームAPIを実装する前に、これらの5つのAPIを作成する必要があります:
saveGame- ゲームの作成または更新queryGames- 保存済みゲームのページネーション付きリストを返すsaveAction- 指定したゲームのアクションを保存queryActions- ゲームに関連する全アクションのページネーション付きリストを返すqueryInventory- プレイヤーのインベントリ内アイテムのページネーション付きリストを返す
APIスキーマ
Section titled “APIスキーマ”APIの入力と出力を定義するため、packages/game-api/src/schemaディレクトリ内でZodを使用してスキーマを作成します:
import { z } from 'zod';
export const ActionSchema = z.object({ playerName: z.string(), timestamp: z.iso.datetime(), role: z.enum(['assistant', 'user']), content: z.string(),});
export type IAction = z.TypeOf<typeof ActionSchema>;import { z } from 'zod';
export const QueryInputSchema = z.object({ cursor: z.string().optional(), limit: z.number().optional().default(100),});
export const createPaginatedQueryOutput = <ItemType extends z.ZodTypeAny>( itemSchema: ItemType,) => { return z.object({ items: z.array(itemSchema), cursor: z.string().nullable(), });};
export type IQueryInput = z.TypeOf<typeof QueryInputSchema>;import { z } from 'zod';
export const GameSchema = z.object({ playerName: z.string(), genre: z.enum(['zombie', 'superhero', 'medieval']), lastUpdated: z.iso.datetime(),});
export type IGame = z.TypeOf<typeof GameSchema>;import { z } from 'zod';
export const ItemSchema = z.object({ playerName: z.string(), itemName: z.string(), emoji: z.string().optional(), lastUpdated: z.iso.datetime(), quantity: z.number(),});
export type IItem = z.TypeOf<typeof ItemSchema>;このプロジェクトで使用しないため、packages/game-api/src/schema/echo.tsファイルは削除できます。
エンティティモデリング
Section titled “エンティティモデリング”これがアプリケーションのER図です。
DynamoDBでデータベースを実装し、ElectroDB DynamoDBクライアントライブラリを使用して簡素化します。electrodbとDynamoDBクライアントをインストールするには、次のコマンドを実行します:
pnpm add -w electrodb@3.5.0 @aws-sdk/client-dynamodb@3.914.0yarn add electrodb@3.5.0 @aws-sdk/client-dynamodb@3.914.0npm install --legacy-peer-deps electrodb@3.5.0 @aws-sdk/client-dynamodb@3.914.0bun install electrodb@3.5.0 @aws-sdk/client-dynamodb@3.914.0ER図からElectroDBエンティティを定義するため、packages/game-api/src/entitiesフォルダ内に以下のファイルを作成します:
import { Entity } from 'electrodb';import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
export const createActionEntity = (client?: DynamoDBClient) => new Entity( { model: { entity: 'Action', version: '1', service: 'game', }, attributes: { playerName: { type: 'string', required: true, readOnly: true }, timestamp: { type: 'string', required: true, readOnly: true, set: () => new Date().toISOString(), default: () => new Date().toISOString(), }, role: { type: 'string', required: true, readOnly: true }, content: { type: 'string', required: true, readOnly: true }, }, indexes: { primary: { pk: { field: 'pk', composite: ['playerName'] }, sk: { field: 'sk', composite: ['timestamp'] }, }, }, }, { client, table: process.env.TABLE_NAME }, );import { Entity } from 'electrodb';import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
export const createGameEntity = (client?: DynamoDBClient) => new Entity( { model: { entity: 'Game', version: '1', service: 'game', }, attributes: { playerName: { type: 'string', required: true, readOnly: true }, genre: { type: 'string', required: true, readOnly: true }, lastUpdated: { type: 'string', required: true, default: () => new Date().toISOString(), }, }, indexes: { primary: { pk: { field: 'pk', composite: ['playerName'] }, sk: { field: 'sk', composite: [], }, }, }, }, { client, table: process.env.TABLE_NAME }, );import { Entity } from 'electrodb';import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
export const createInventoryEntity = (client?: DynamoDBClient) => new Entity( { model: { entity: 'Inventory', version: '1', service: 'game', }, attributes: { playerName: { type: 'string', required: true, readOnly: true }, lastUpdated: { type: 'string', required: true, default: () => new Date().toISOString(), }, itemName: { type: 'string', required: true, }, emoji: { type: 'string', required: false, }, quantity: { type: 'number', required: true, }, }, indexes: { primary: { pk: { field: 'pk', composite: ['playerName'] }, sk: { field: 'sk', composite: ['itemName'] }, }, }, }, { client, table: process.env.TABLE_NAME }, );ElectroDBでは型定義だけでなく、タイムスタンプなどの特定の値にデフォルト値を設定できます。またElectroDBはDynamoDB使用時のベストプラクティスであるシングルテーブル設計に従います。
MCPサーバーがインベントリと連携できるよう、packages/game-api/src/index.tsでインベントリエンティティをエクスポートします:
export type { AppRouter } from './router.js';export { appRouter } from './router.js';export type { Context } from './init.js';export * from './client/index.js';export * from './schema/index.js';export * from './entities/inventory.js';tRPCコンテキストへのDynamoDBクライアント追加
Section titled “tRPCコンテキストへのDynamoDBクライアント追加”各プロシージャでDynamoDBクライアントにアクセスする必要があるため、コンテキスト経由で渡せるクライアントの単一インスタンスを作成します。これを行うため、packages/game-api/src内で以下の変更を加えます:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { initTRPC } from '@trpc/server';
export interface IDynamoDBContext { dynamoDb?: DynamoDBClient;}
export const createDynamoDBPlugin = () => { const t = initTRPC.context<IDynamoDBContext>().create(); return t.procedure.use(async (opts) => { const dynamoDb = new DynamoDBClient();
const response = await opts.next({ ctx: { ...opts.ctx, dynamoDb, }, });
return response; });};DynamoDBClientを作成しコンテキストに注入するプラグインです。
import { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';import type { APIGatewayProxyEvent } from 'aws-lambda';import { ILoggerContext } from './logger.js';import { IMetricsContext } from './metrics.js';import { ITracerContext } from './tracer.js';import { IDynamoDBContext } from './dynamodb.js';
export * from './dynamodb.js';export * from './logger.js';export * from './metrics.js';export * from './tracer.js';export * from './error.js';
export type IMiddlewareContext = CreateAWSLambdaContextOptions<APIGatewayProxyEvent> & IDynamoDBContext & ILoggerContext & IMetricsContext & ITracerContext;IMiddlewareContextを拡張してIDynamoDBContextを追加します。
import { initTRPC } from '@trpc/server';import { createDynamoDBPlugin, createErrorPlugin, createLoggerPlugin, createMetricsPlugin, createTracerPlugin, IMiddlewareContext,} from './middleware/index.js';
process.env.POWERTOOLS_SERVICE_NAME = 'GameApi';process.env.POWERTOOLS_METRICS_NAMESPACE = 'GameApi';
export type Context = IMiddlewareContext;
export const t = initTRPC.context<Context>().create();
export const publicProcedure = t.procedure .concat(createDynamoDBPlugin()) .concat(createLoggerPlugin()) .concat(createTracerPlugin()) .concat(createMetricsPlugin()) .concat(createErrorPlugin());これによりDynamoDBプラグインが組み込まれます。
プロシージャの定義
Section titled “プロシージャの定義”APIメソッドを実装するため、packages/game-api/src/procedures内で以下の変更を加えます:
import { createActionEntity } from '../entities/action.js';import { ActionSchema, IAction, QueryInputSchema, createPaginatedQueryOutput,} from '../schema/index.js';import { publicProcedure } from '../init.js';import { z } from 'zod';
export const queryActions = publicProcedure .input(QueryInputSchema.extend({ playerName: z.string() })) .output(createPaginatedQueryOutput(ActionSchema)) .query(async ({ input, ctx }) => { const actionEntity = createActionEntity(ctx.dynamoDb); const result = await actionEntity.query .primary({ playerName: input.playerName }) .go({ cursor: input.cursor, count: input.limit });
return { items: result.data as IAction[], cursor: result.cursor, }; });import { createGameEntity } from '../entities/game.js';import { GameSchema, IGame, QueryInputSchema, createPaginatedQueryOutput,} from '../schema/index.js';import { publicProcedure } from '../init.js';
export const queryGames = publicProcedure .input(QueryInputSchema) .output(createPaginatedQueryOutput(GameSchema)) .query(async ({ input, ctx }) => { const gameEntity = createGameEntity(ctx.dynamoDb); const result = await gameEntity.scan.go({ cursor: input.cursor, count: input.limit, });
return { items: result.data as IGame[], cursor: result.cursor, }; });import { ItemSchema, QueryInputSchema, createPaginatedQueryOutput,} from '../schema/index.js';import { publicProcedure } from '../init.js';import { z } from 'zod';import { createInventoryEntity } from '../entities/inventory.js';
export const queryInventory = publicProcedure .input(QueryInputSchema.extend({ playerName: z.string() })) .output(createPaginatedQueryOutput(ItemSchema)) .query(async ({ input, ctx }) => { const inventoryEntity = createInventoryEntity(ctx.dynamoDb); const result = await inventoryEntity.query .primary({ playerName: input.playerName }) .go({ cursor: input.cursor, count: input.limit });
return { items: result.data, cursor: result.cursor, }; });ミューテーション
Section titled “ミューテーション”import { ActionSchema, IAction } from '../schema/index.js';import { publicProcedure } from '../init.js';import { createActionEntity } from '../entities/action.js';import { createGameEntity } from '../entities/game.js';
export const saveAction = publicProcedure .input(ActionSchema.omit({ timestamp: true })) .output(ActionSchema) .mutation(async ({ input, ctx }) => { const actionEntity = createActionEntity(ctx.dynamoDb); const gameEntity = createGameEntity(ctx.dynamoDb);
const action = await actionEntity.put(input).go(); await gameEntity .update({ playerName: input.playerName }) .set({ lastUpdated: action.data.timestamp }) .go(); return action.data as IAction; });import { createGameEntity } from '../entities/game.js';import { GameSchema, IGame } from '../schema/index.js';import { publicProcedure } from '../init.js';
export const saveGame = publicProcedure .input(GameSchema.omit({ lastUpdated: true })) .output(GameSchema) .mutation(async ({ input, ctx }) => { const gameEntity = createGameEntity(ctx.dynamoDb);
const result = await gameEntity.put(input).go(); return result.data as IGame; });このプロジェクトで使用しないため、echo.tsファイル(packages/game-api/src/procedures内)は削除できます。
ルーター設定
Section titled “ルーター設定”プロシージャを定義した後、APIに接続するため、以下のファイルを更新します:
import { awsLambdaRequestHandler, CreateAWSLambdaContextOptions,} from '@trpc/server/adapters/aws-lambda';import { t } from './init.js';import { APIGatewayProxyEvent } from 'aws-lambda';import { queryActions } from './procedures/query-actions.js';import { saveAction } from './procedures/save-action.js';import { queryGames } from './procedures/query-games.js';import { saveGame } from './procedures/save-game.js';import { queryInventory } from './procedures/query-inventory.js';
export const router = t.router;
export const appRouter = router({ actions: router({ query: queryActions, save: saveAction, }), games: router({ query: queryGames, save: saveGame, }), inventory: router({ query: queryInventory, }),});
export const handler = awsLambdaRequestHandler({ router: appRouter, createContext: ( ctx: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>, ) => ctx, responseMeta: () => ({ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', }, }),});
export type AppRouter = typeof appRouter;import { awsLambdaRequestHandler, CreateAWSLambdaContextOptions,} from '@trpc/server/adapters/aws-lambda';import { echo } from './procedures/echo.js';import { t } from './init.js';import { APIGatewayProxyEvent } from 'aws-lambda';import { queryActions } from './procedures/query-actions.js';import { saveAction } from './procedures/save-action.js';import { queryGames } from './procedures/query-games.js';import { saveGame } from './procedures/save-game.js';import { queryInventory } from './procedures/query-inventory.js';
export const router = t.router;
export const appRouter = router({ echo, actions: router({ query: queryActions, save: saveAction, }), games: router({ query: queryGames, save: saveGame, }), inventory: router({ query: queryInventory, }),});
export const handler = awsLambdaRequestHandler({ router: appRouter, createContext: ( ctx: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>, ) => ctx, responseMeta: () => ({ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', }, }),});
export type AppRouter = typeof appRouter;タスク2: インベントリMCPサーバーの作成
Section titled “タスク2: インベントリMCPサーバーの作成”エージェントがプレイヤーのインベントリのアイテムを管理できるMCPサーバーを作成します。
エージェント用に以下のツールを定義します:
list-inventory-items- プレイヤーの現在のインベントリアイテムを取得add-to-inventory- プレイヤーのインベントリにアイテムを追加remove-from-inventory- プレイヤーのインベントリからアイテムを削除
時間節約のため、すべてのツールをインラインで定義します:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import z from 'zod-v3';import { createInventoryEntity } from ':dungeon-adventure/game-api';
/** * Create the MCP Server */export const createServer = () => { const server = new McpServer({ name: 'inventory-mcp-server', version: '1.0.0', });
const dynamoDb = new DynamoDBClient(); const inventory = createInventoryEntity(dynamoDb);
server.tool( 'list-inventory-items', "List items in the player's inventory. Leave cursor blank unless you are requesting subsequent pages", { playerName: z.string(), cursor: z.string().optional(), }, async ({ playerName }) => { const results = await inventory.query .primary({ playerName, }) .go();
return { content: [{ type: 'text', text: JSON.stringify(results) }], }; }, );
server.tool( 'add-to-inventory', "Add an item to the player's inventory. Quantity defaults to 1 if omitted.", { playerName: z.string(), itemName: z.string(), emoji: z.string(), quantity: z.number().optional().default(1), }, async ({ playerName, itemName, emoji, quantity = 1 }) => { await inventory .put({ playerName, itemName, quantity, emoji, }) .go();
return { content: [ { type: 'text', text: `Added ${itemName} (x${quantity}) to inventory`, }, ], }; }, );
server.tool( 'remove-from-inventory', "Remove an item from the player's inventory. If quantity is omitted, all items are removed.", { playerName: z.string(), itemName: z.string(), quantity: z.number().optional(), }, async ({ playerName, itemName, quantity }) => { // If quantity is omitted, remove the entire item if (quantity === undefined) { try { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text', text: `${itemName} removed from inventory.` }, ], } as const; } catch { return { content: [ { type: 'text', text: `${itemName} not found in inventory` }, ], } as const; } }
// If quantity is specified, fetch current quantity and update const item = await inventory.get({ playerName, itemName }).go();
if (!item.data) { return { content: [ { type: 'text', text: `${itemName} not found in inventory` }, ], } as const; }
const newQuantity = item.data.quantity - quantity;
if (newQuantity <= 0) { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text', text: `${itemName} removed from inventory.` }, ], } as const; }
await inventory .put({ playerName, itemName, quantity: newQuantity, emoji: item.data.emoji, }) .go();
return { content: [ { type: 'text', text: `Removed ${itemName} (x${quantity}) from inventory. ${newQuantity} remaining.`, }, ], }; }, );
return server;};import { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import { registerAddTool } from './tools/add.js';import { registerSampleGuidanceResource } from './resources/sample-guidance.js';import z from 'zod-v3';import { createInventoryEntity } from ':dungeon-adventure/game-api';
/** * Create the MCP Server */export const createServer = () => { const server = new McpServer({ name: 'inventory-mcp-server', version: '1.0.0', });
registerAddTool(server); registerSampleGuidanceResource(server); const dynamoDb = new DynamoDBClient(); const inventory = createInventoryEntity(dynamoDb);
server.tool( 'list-inventory-items', "List items in the player's inventory. Leave cursor blank unless you are requesting subsequent pages", { playerName: z.string(), cursor: z.string().optional(), }, async ({ playerName }) => { const results = await inventory.query .primary({ playerName, }) .go();
return { content: [{ type: 'text', text: JSON.stringify(results) }], }; }, );
server.tool( 'add-to-inventory', "Add an item to the player's inventory. Quantity defaults to 1 if omitted.", { playerName: z.string(), itemName: z.string(), emoji: z.string(), quantity: z.number().optional().default(1), }, async ({ playerName, itemName, emoji, quantity = 1 }) => { await inventory .put({ playerName, itemName, quantity, emoji, }) .go();
return { content: [ { type: 'text', text: `Added ${itemName} (x${quantity}) to inventory`, }, ], }; }, );
server.tool( 'remove-from-inventory', "Remove an item from the player's inventory. If quantity is omitted, all items are removed.", { playerName: z.string(), itemName: z.string(), quantity: z.number().optional(), }, async ({ playerName, itemName, quantity }) => { // If quantity is omitted, remove the entire item if (quantity === undefined) { try { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text', text: `${itemName} removed from inventory.` }, ], } as const; } catch { return { content: [ { type: 'text', text: `${itemName} not found in inventory` }, ], } as const; } }
// If quantity is specified, fetch current quantity and update const item = await inventory.get({ playerName, itemName }).go();
if (!item.data) { return { content: [ { type: 'text', text: `${itemName} not found in inventory` }, ], } as const; }
const newQuantity = item.data.quantity - quantity;
if (newQuantity <= 0) { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text', text: `${itemName} removed from inventory.` }, ], } as const; }
await inventory .put({ playerName, itemName, quantity: newQuantity, emoji: item.data.emoji, }) .go();
return { content: [ { type: 'text', text: `Removed ${itemName} (x${quantity}) from inventory. ${newQuantity} remaining.`, }, ], }; }, );
return server;};ツール数が増えた場合、必要に応じて別ファイルにリファクタリングできます。
packages/inventory/src/mcp-server内のtoolsとresourcesディレクトリは使用しないため削除してください。
タスク3: インフラストラクチャの更新
Section titled “タスク3: インフラストラクチャの更新”最後のステップは、DynamoDBテーブルを作成し、Game APIからの操作権限を付与するためインフラストラクチャを更新することです。
これを行うため、packages/infra/srcを以下のように更新します:
import { CfnOutput } from 'aws-cdk-lib';import { AttributeType, BillingMode, ProjectionType, Table, TableProps,} from 'aws-cdk-lib/aws-dynamodb';import { Construct } from 'constructs';import { suppressRules } from ':dungeon-adventure/common-constructs';
export type ElectrodbDynamoTableProps = Omit< TableProps, 'partitionKey' | 'sortKey' | 'billingMode'>;
export class ElectrodbDynamoTable extends Table { constructor(scope: Construct, id: string, props?: ElectrodbDynamoTableProps) { super(scope, id, { partitionKey: { name: 'pk', type: AttributeType.STRING, }, sortKey: { name: 'sk', type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, ...props, });
this.addGlobalSecondaryIndex({ indexName: 'gsi1pk-gsi1sk-index', partitionKey: { name: 'gsi1pk', type: AttributeType.STRING, }, sortKey: { name: 'gsi1sk', type: AttributeType.STRING, }, projectionType: ProjectionType.ALL, });
// Suppress checkov rules that expect a KMS customer managed key and backup to be enabled suppressRules(this, ['CKV_AWS_119', 'CKV_AWS_28'], 'No need for custom encryption or backup');
new CfnOutput(this, 'TableName', { value: this.tableName }); }}import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';import { ElectrodbDynamoTable } from '../constructs/electrodb-table.js';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const userIdentity = new UserIdentity(this, 'UserIdentity');
const electroDbTable = new ElectrodbDynamoTable(this, 'ElectroDbTable');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this) .withDefaultOptions({ environment: { TABLE_NAME: electroDbTable.tableName, }, }) .build(), });
electroDbTable.grantReadData(gameApi.integrations['actions.query'].handler); electroDbTable.grantReadData(gameApi.integrations['games.query'].handler); electroDbTable.grantReadData(gameApi.integrations['inventory.query'].handler); electroDbTable.grantReadWriteData( gameApi.integrations['actions.save'].handler, ); electroDbTable.grantReadWriteData( gameApi.integrations['games.save'].handler, );
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer', { environment: { TABLE_NAME: electroDbTable.tableName, }, }); electroDbTable.grantReadWriteData(mcpServer.agentCoreRuntime);
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';import { ElectrodbDynamoTable } from '../constructs/electrodb-table.js';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const userIdentity = new UserIdentity(this, 'UserIdentity');
const electroDbTable = new ElectrodbDynamoTable(this, 'ElectroDbTable');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), integrations: GameApi.defaultIntegrations(this) .withDefaultOptions({ environment: { TABLE_NAME: electroDbTable.tableName, }, }) .build(), });
electroDbTable.grantReadData(gameApi.integrations['actions.query'].handler); electroDbTable.grantReadData(gameApi.integrations['games.query'].handler); electroDbTable.grantReadData(gameApi.integrations['inventory.query'].handler); electroDbTable.grantReadWriteData( gameApi.integrations['actions.save'].handler, ); electroDbTable.grantReadWriteData( gameApi.integrations['games.save'].handler, );
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer'); const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer', { environment: { TABLE_NAME: electroDbTable.tableName, }, }); electroDbTable.grantReadWriteData(mcpServer.agentCoreRuntime);
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}タスク4: デプロイとテスト
Section titled “タスク4: デプロイとテスト”コードベースをビルドするには:
pnpm nx run-many --target build --allyarn nx run-many --target build --allnpx nx run-many --target build --allbunx nx run-many --target build --allアプリケーションのデプロイ
Section titled “アプリケーションのデプロイ”アプリケーションをデプロイするには、以下のコマンドを実行します:
pnpm nx deploy infra dungeon-adventure-infra-sandbox/*yarn nx deploy infra dungeon-adventure-infra-sandbox/*npx nx deploy infra dungeon-adventure-infra-sandbox/*bunx nx deploy infra dungeon-adventure-infra-sandbox/*初回デプロイは約8分かかります。以降のデプロイは約2分です。
デプロイ完了後、以下のような出力が表示されます(一部値は編集済み):
dungeon-adventure-sandbox-Applicationdungeon-adventure-sandbox-Application: deploying... [2/2]
✅ dungeon-adventure-sandbox-Application
✨ Deployment time: 354s
Outputs:dungeon-adventure-sandbox-Application.ElectroDbTableTableNameXXX = dungeon-adventure-sandbox-Application-ElectroDbTableXXX-YYYdungeon-adventure-sandbox-Application.GameApiEndpointXXX = https://xxx.execute-api.region.amazonaws.com/prod/dungeon-adventure-sandbox-Application.GameUIDistributionDomainNameXXX = xxx.cloudfront.netdungeon-adventure-sandbox-Application.StoryApiEndpointXXX = https://xxx.execute-api.region.amazonaws.com/prod/dungeon-adventure-sandbox-Application.UserIdentityUserIdentityIdentityPoolIdXXX = region:xxxdungeon-adventure-sandbox-Application.UserIdentityUserIdentityUserPoolIdXXX = region_xxxAPIのテスト
Section titled “APIのテスト”APIは以下の方法でテストできます:
- tRPCバックエンドのローカルインスタンスを起動し
curlでAPIを呼び出す - デプロイ済みAPIをsigv4対応curlで呼び出す
Sigv4対応curl
.bashrcファイルに以下のスクリプトを追加(sourceで適用)するか、コマンドを実行するターミナルに直接貼り付けます。~/.bashrc acurl () {REGION=$1SERVICE=$2shift; shift;curl --aws-sigv4 "aws:amz:$REGION:$SERVICE" --user "$(aws configure get aws_access_key_id):$(aws configure get aws_secret_access_key)" -H "X-Amz-Security-Token: $(aws configure get aws_session_token)" "$@"}sigv4認証済みcurlリクエストを実行するには、以下の例のようにacurlを呼び出します:API Gateway
Section titled “API Gateway”Terminal window acurl ap-southeast-2 execute-api -X GET https://xxxストリーミングLambda関数URL
Section titled “ストリーミングLambda関数URL”Terminal window acurl ap-southeast-2 lambda -N -X POST https://xxxPowerShellプロファイルに関数を追加するか、現在のセッションに直接貼り付けます。
Terminal window # PowerShell profile or current sessionfunction acurl {param([Parameter(Mandatory=$true)][string]$Region,[Parameter(Mandatory=$true)][string]$Service,[Parameter(ValueFromRemainingArguments=$true)][string[]]$CurlArgs)$AccessKey = aws configure get aws_access_key_id$SecretKey = aws configure get aws_secret_access_key$SessionToken = aws configure get aws_session_token& curl --aws-sigv4 "aws:amz:$Region`:$Service" --user "$AccessKey`:$SecretKey" -H "X-Amz-Security-Token: $SessionToken" @CurlArgs}sigv4認証済みcurlリクエストを実行するには、以下の例のようにacurlを呼び出します:API Gateway
Section titled “API Gateway”Terminal window acurl ap-southeast-2 execute-api -X GET https://xxxストリーミングLambda関数URL
Section titled “ストリーミングLambda関数URL”Terminal window acurl ap-southeast-2 lambda -N -X POST https://xxx
ローカルのgame-apiサーバーを起動するには、以下のコマンドを実行します:
TABLE_NAME=dungeon-adventure-infra-sandbox-Application-ElectroDbTableXXX-YYY pnpm nx run @dungeon-adventure/game-api:serveTABLE_NAME=dungeon-adventure-infra-sandbox-Application-ElectroDbTableXXX-YYY yarn nx run @dungeon-adventure/game-api:serveTABLE_NAME=dungeon-adventure-infra-sandbox-Application-ElectroDbTableXXX-YYY npx nx run @dungeon-adventure/game-api:serveTABLE_NAME=dungeon-adventure-infra-sandbox-Application-ElectroDbTableXXX-YYY bunx nx run @dungeon-adventure/game-api:serveサーバーが起動したら、以下のコマンドで呼び出せます:
curl -X GET 'http://localhost:2022/games.query?input=%7B%7D'acurl ap-southeast-2 execute-api -X GET 'https://xxx.execute-api.ap-southeast-2.amazonaws.com/prod/games.query?input=%7B%7D'コマンドが成功すると、以下のようなレスポンスが返ります:
{"result":{"data":{"items":[],"cursor":null}}}おめでとうございます!tRPCを使用した最初のAPIの構築とデプロイに成功しました! 🎉🎉🎉