Triển khai Game API và Inventory MCP server
Nhiệm vụ 1: Triển khai Game API
Phần tiêu đề “Nhiệm vụ 1: Triển khai Game API”Chúng ta sẽ triển khai các API sau trong phần này:
saveGame- tạo hoặc cập nhật một trò chơi.queryGames- trả về danh sách phân trang các trò chơi đã lưu trước đó.queryInventory- trả về danh sách phân trang các vật phẩm trong kho của người chơi.queryActions- trả về lịch sử hội thoại cho một trò chơi nhất định.
Schema API
Phần tiêu đề “Schema API”Để định nghĩa đầu vào và đầu ra của API, hãy tạo schema của chúng ta bằng Zod trong file packages/game-api/src/schema/index.ts như sau:
import { z } from 'zod';
export const QueryInputSchema = z.object({ cursor: z.string().optional(), limit: z.number().optional().default(100),});export type IQueryInput = z.TypeOf<typeof QueryInputSchema>;
export const ActionSchema = z.object({ role: z.enum(['user', 'assistant']), content: z.string(), messageId: z.number(),});export type IAction = z.TypeOf<typeof ActionSchema>;
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>;
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>;
export const createPaginatedQueryOutput = <ItemType extends z.ZodTypeAny>( itemSchema: ItemType,) => { return z.object({ items: z.array(itemSchema), cursor: z.string().nullable(), });};export * from './echo.js'import { z } from 'zod';
export const QueryInputSchema = z.object({ cursor: z.string().optional(), limit: z.number().optional().default(100),});export type IQueryInput = z.TypeOf<typeof QueryInputSchema>;
export const ActionSchema = z.object({ role: z.enum(['user', 'assistant']), content: z.string(), messageId: z.number(),});export type IAction = z.TypeOf<typeof ActionSchema>;
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>;
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>;
export const createPaginatedQueryOutput = <ItemType extends z.ZodTypeAny>( itemSchema: ItemType,) => { return z.object({ items: z.array(itemSchema), cursor: z.string().nullable(), });};Xóa file packages/game-api/src/schema/echo.ts vì chúng ta sẽ không sử dụng nó trong dự án này.
Mô hình hóa thực thể
Phần tiêu đề “Mô hình hóa thực thể”Đây là sơ đồ ER cho ứng dụng của chúng ta.
Generator ts#dynamodb đã thiết lập ElectroDB, mà chúng ta sẽ sử dụng để mô hình hóa dữ liệu. Chúng ta sẽ lưu trữ lịch sử hội thoại trong S3, vì vậy chúng ta thêm dependency vào S3 client:
pnpm add -w @aws-sdk/client-s3@3.1085.0yarn add @aws-sdk/client-s3@3.1085.0npm install --legacy-peer-deps @aws-sdk/client-s3@3.1085.0bun install @aws-sdk/client-s3@3.1085.0Thay thế entity mẫu được tạo trong packages/dungeon-db/src/entities/index.ts bằng các entities Game và Inventory của chúng ta, và xóa packages/dungeon-db/src/entities/example.ts:
import { Entity } from 'electrodb';import { getDynamoDBClient, resolveTableName } from '../client.js';
export const createGameEntity = async () => 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: getDynamoDBClient(), table: await resolveTableName() }, );
export const createInventoryEntity = async () => 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: getDynamoDBClient(), table: await resolveTableName() }, );ElectroDB cho phép chúng ta không chỉ định nghĩa các kiểu của mình mà còn có thể cung cấp giá trị mặc định cho các giá trị nhất định như timestamps. Ngoài ra, ElectroDB tuân theo single-table design là best practice khi sử dụng DynamoDB.
Định nghĩa các procedures
Phần tiêu đề “Định nghĩa các procedures”Để triển khai các phương thức API, thực hiện các thay đổi sau trong packages/game-api/src/procedures:
import { createGameEntity } from ':dungeon-adventure/dungeon-db';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 }) => { const gameEntity = await createGameEntity(); const result = await gameEntity.scan.go({ cursor: input.cursor, count: input.limit, });
return { items: result.data as IGame[], cursor: result.cursor, }; });
export const saveGame = publicProcedure .input(GameSchema.omit({ lastUpdated: true })) .output(GameSchema) .mutation(async ({ input }) => { const gameEntity = await createGameEntity();
const result = await gameEntity.put(input).go(); return result.data as IGame; });import { ItemSchema, QueryInputSchema, createPaginatedQueryOutput,} from '../schema/index.js';import { publicProcedure } from '../init.js';import { z } from 'zod';import { createInventoryEntity } from ':dungeon-adventure/dungeon-db';
export const queryInventory = publicProcedure .input(QueryInputSchema.extend({ playerName: z.string() })) .output(createPaginatedQueryOutput(ItemSchema)) .query(async ({ input }) => { const inventoryEntity = await createInventoryEntity(); const result = await inventoryEntity.query .primary({ playerName: input.playerName }) .go({ cursor: input.cursor, count: input.limit });
return { items: result.data, cursor: result.cursor, }; });import { publicProcedure } from '../init.js';import { ActionSchema, IAction } from '../schema/index.js';import { z } from 'zod';import { S3Client, ListObjectsV2Command, GetObjectCommand,} from '@aws-sdk/client-s3';import { getAppConfig } from '@aws-lambda-powertools/parameters/appconfig';import { readFile, readdir } from 'node:fs/promises';import { join, resolve } from 'node:path';
const resolveSessionsBucket = async (): Promise<string> => { const buckets = await getAppConfig('buckets', { application: process.env.RUNTIME_CONFIG_APP_ID!, environment: 'default', transform: 'json', }); const bucket = (buckets as Record<string, any>).StorySessions?.bucketName; if (!bucket) throw new Error('StorySessions bucket not found in runtime config'); return bucket;};
const s3 = new S3Client();const LOCAL_SESSION_STORAGE_DIR = '/tmp/strands-sessions';
// Matches ``session_<sessionId>/agents/agent_<agentId>/messages/message_<idx>.json``// written by ``strands.session.S3SessionManager`` on the agent side. We only// ever care about the default agent id ``default`` that Strands uses when// none is set explicitly, so hard-code the path prefix the UI needs to list.const messagesPrefix = (sessionId: string) => `session_${sessionId}/agents/agent_default/messages/`;
const messageIndex = (pathOrKey: string): number => { const match = pathOrKey.match(/message_(\d+)\.json$/); return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;};
const toAction = (body: any): IAction | undefined => { const message = body.redact_message ?? body.message; const role = message?.role; if (role !== 'user' && role !== 'assistant') return undefined; const text = Array.isArray(message.content) ? message.content .filter((b: any) => typeof b?.text === 'string') .map((b: any) => b.text) .join('') : String(message.content ?? ''); if (!text) return undefined; return { role, content: text, messageId: body.message_id };};
const readLocalActions = async (sessionId: string): Promise<IAction[]> => { const baseDir = resolve(LOCAL_SESSION_STORAGE_DIR); const messagesDir = resolve(baseDir, messagesPrefix(sessionId)); if (!messagesDir.startsWith(`${baseDir}/`)) { throw new Error('Invalid session id'); }
let files: string[]; try { files = await readdir(messagesDir); } catch (error: any) { if (error?.code === 'ENOENT') return []; throw error; }
const actions: IAction[] = []; for (const file of files .filter((f) => f.endsWith('.json')) .sort((a, b) => messageIndex(a) - messageIndex(b))) { const body = JSON.parse(await readFile(join(messagesDir, file), 'utf8')); const action = toAction(body); if (action) actions.push(action); } return actions;};
const readS3Actions = async (sessionId: string): Promise<IAction[]> => { const bucket = await resolveSessionsBucket(); const list = await s3.send( new ListObjectsV2Command({ Bucket: bucket, Prefix: messagesPrefix(sessionId), }), ); const keys = (list.Contents ?? []) .map((o) => o.Key!) .filter((k) => k.endsWith('.json')) .sort((a, b) => messageIndex(a) - messageIndex(b));
const actions: IAction[] = []; for (const key of keys) { const obj = await s3.send( new GetObjectCommand({ Bucket: bucket, Key: key }), ); const body = JSON.parse(await obj.Body!.transformToString()); const action = toAction(body); if (action) actions.push(action); } return actions;};
export const queryActions = publicProcedure .input(z.object({ sessionId: z.string() })) .output(z.object({ items: z.array(ActionSchema) })) .query(async ({ input }) => { const actions = process.env.LOCAL_DEV === 'true' ? await readLocalActions(input.sessionId) : await readS3Actions(input.sessionId); return { items: actions }; });Xóa file echo.ts (từ packages/game-api/src/procedures) vì chúng ta sẽ không sử dụng nó trong dự án này.
Thiết lập Router
Phần tiêu đề “Thiết lập Router”Sau khi chúng ta định nghĩa các procedures, để kết nối chúng vào API của chúng ta, cập nhật file sau:
import { t } from './init.js';import { queryActions } from './procedures/actions.js';import { queryGames, saveGame } from './procedures/games.js';import { queryInventory } from './procedures/inventory.js';
export const router = t.router;
export const appRouter = router({ actions: router({ query: queryActions, }), games: router({ query: queryGames, save: saveGame, }), inventory: router({ query: queryInventory, }),});
export type AppRouter = typeof appRouter;import { echo } from './procedures/echo.js';import { t } from './init.js';import { queryActions } from './procedures/actions.js';import { queryGames, saveGame } from './procedures/games.js';import { queryInventory } from './procedures/inventory.js';
export const router = t.router;
export const appRouter = router({ echo, actions: router({ query: queryActions, }), games: router({ query: queryGames, save: saveGame, }), inventory: router({ query: queryInventory, }),});
export type AppRouter = typeof appRouter;Nhiệm vụ 2: Tạo Inventory MCP server
Phần tiêu đề “Nhiệm vụ 2: Tạo Inventory MCP server”Hãy tạo một MCP server cho phép agent của chúng ta quản lý các vật phẩm trong kho của người chơi.
Chúng ta sẽ định nghĩa các công cụ sau cho agent của chúng ta:
list-inventory-itemsđể lấy các vật phẩm kho hiện tại của người chơiadd-to-inventoryđể thêm vật phẩm vào kho của người chơiremove-from-inventoryđể xóa vật phẩm khỏi kho của người chơi
Để tiết kiệm thời gian, chúng ta sẽ định nghĩa tất cả các công cụ inline:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import z from 'zod';import { createInventoryEntity } from ':dungeon-adventure/dungeon-db';
/** * Create the MCP Server */export const createServer = async () => { const server = new McpServer({ name: 'inventory-mcp-server', version: '1.0.0', });
server.registerTool( 'list-inventory-items', { description: "List items in the player's inventory. Leave cursor blank unless you are requesting subsequent pages", inputSchema: { playerName: z.string(), cursor: z.string().optional(), }, }, async ({ playerName }) => { const inventory = await createInventoryEntity(); const results = await inventory.query .primary({ playerName, }) .go();
return { content: [{ type: 'text' as const, text: JSON.stringify(results) }], }; }, );
server.registerTool( 'add-to-inventory', { description: "Add an item to the player's inventory. Quantity defaults to 1 if omitted.", inputSchema: { playerName: z.string(), itemName: z.string(), emoji: z.string(), quantity: z.number().optional().default(1), }, }, async ({ playerName, itemName, emoji, quantity = 1 }) => { const inventory = await createInventoryEntity(); await inventory .put({ playerName, itemName, quantity, emoji, }) .go();
return { content: [ { type: 'text' as const, text: `Added ${itemName} (x${quantity}) to inventory`, }, ], }; }, );
server.registerTool( 'remove-from-inventory', { description: "Remove an item from the player's inventory. If quantity is omitted, all items are removed.", inputSchema: { playerName: z.string(), itemName: z.string(), quantity: z.number().optional(), }, }, async ({ playerName, itemName, quantity }) => { const inventory = await createInventoryEntity();
// If quantity is omitted, remove the entire item if (quantity === undefined) { try { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text' as const, text: `${itemName} removed from inventory.` }, ], }; } catch { return { content: [ { type: 'text' as const, text: `${itemName} not found in inventory` }, ], }; } }
// If quantity is specified, fetch current quantity and update const item = await inventory.get({ playerName, itemName }).go();
if (!item.data) { return { content: [ { type: 'text' as const, text: `${itemName} not found in inventory` }, ], }; }
const newQuantity = item.data.quantity - quantity;
if (newQuantity <= 0) { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text' as const, text: `${itemName} removed from inventory.` }, ], }; }
await inventory .put({ playerName, itemName, quantity: newQuantity, emoji: item.data.emoji, }) .go();
return { content: [ { type: 'text' as const, text: `Removed ${itemName} (x${quantity}) from inventory. ${newQuantity} remaining.`, }, ], }; }, );
return server;};import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';import { registerDivideTool } from './tools/divide.js';import { registerSampleGuidanceResource } from './resources/sample-guidance.js';import z from 'zod';import { createInventoryEntity } from ':dungeon-adventure/dungeon-db';
/** * Create the MCP Server */export const createServer = async () => { const server = new McpServer({ name: 'inventory-mcp-server', version: '1.0.0', });
registerDivideTool(server); registerSampleGuidanceResource(server); server.registerTool( 'list-inventory-items', { description: "List items in the player's inventory. Leave cursor blank unless you are requesting subsequent pages", inputSchema: { playerName: z.string(), cursor: z.string().optional(), }, }, async ({ playerName }) => { const inventory = await createInventoryEntity(); const results = await inventory.query .primary({ playerName, }) .go();
return { content: [{ type: 'text' as const, text: JSON.stringify(results) }], }; }, );
server.registerTool( 'add-to-inventory', { description: "Add an item to the player's inventory. Quantity defaults to 1 if omitted.", inputSchema: { playerName: z.string(), itemName: z.string(), emoji: z.string(), quantity: z.number().optional().default(1), }, }, async ({ playerName, itemName, emoji, quantity = 1 }) => { const inventory = await createInventoryEntity(); await inventory .put({ playerName, itemName, quantity, emoji, }) .go();
return { content: [ { type: 'text' as const, text: `Added ${itemName} (x${quantity}) to inventory`, }, ], }; }, );
server.registerTool( 'remove-from-inventory', { description: "Remove an item from the player's inventory. If quantity is omitted, all items are removed.", inputSchema: { playerName: z.string(), itemName: z.string(), quantity: z.number().optional(), }, }, async ({ playerName, itemName, quantity }) => { const inventory = await createInventoryEntity();
// If quantity is omitted, remove the entire item if (quantity === undefined) { try { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text' as const, text: `${itemName} removed from inventory.` }, ], }; } catch { return { content: [ { type: 'text' as const, text: `${itemName} not found in inventory` }, ], }; } }
// If quantity is specified, fetch current quantity and update const item = await inventory.get({ playerName, itemName }).go();
if (!item.data) { return { content: [ { type: 'text' as const, text: `${itemName} not found in inventory` }, ], }; }
const newQuantity = item.data.quantity - quantity;
if (newQuantity <= 0) { await inventory.delete({ playerName, itemName }).go(); return { content: [ { type: 'text' as const, text: `${itemName} removed from inventory.` }, ], }; }
await inventory .put({ playerName, itemName, quantity: newQuantity, emoji: item.data.emoji, }) .go();
return { content: [ { type: 'text' as const, text: `Removed ${itemName} (x${quantity}) from inventory. ${newQuantity} remaining.`, }, ], }; }, );
return server;};Khi số lượng công cụ tăng lên, bạn có thể tái cấu trúc chúng thành các file riêng biệt nếu muốn.
Xóa các thư mục tools và resources trong packages/inventory/src/mcp-server vì chúng sẽ không được sử dụng.
Nhiệm vụ 3: Cập nhật cơ sở hạ tầng
Phần tiêu đề “Nhiệm vụ 3: Cập nhật cơ sở hạ tầng”Construct DungeonDb được tạo bởi ts#dynamodb đã cung cấp bảng của chúng ta, vì vậy chúng ta chỉ cần khởi tạo nó trong stack và cấp cho Game API và Inventory MCP server các quyền mà chúng cần. Cập nhật packages/infra/src/stacks/application-stack.ts như sau:
import { DungeonDb, GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity, suppressRules,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput, RemovalPolicy } from 'aws-cdk-lib';import { BlockPublicAccess, Bucket, BucketEncryption,} from 'aws-cdk-lib/aws-s3';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const rc = RuntimeConfig.ensure(this);
const userIdentity = new UserIdentity(this, 'UserIdentity');
// Sandbox-friendly: allow the table to be deleted with the stack. const dungeonDb = new DungeonDb(this, 'DungeonDb', { deletionProtection: false, removalPolicy: RemovalPolicy.DESTROY, });
// S3 bucket for Strands conversation history. The Story Agent writes each // turn via ``S3SessionManager``; the Game API reads them back for replay. const storySessions = new Bucket(this, 'StorySessions', { encryption: BucketEncryption.S3_MANAGED, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, enforceSSL: true, removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, }); suppressRules( storySessions, ['CKV_AWS_18', 'CKV_AWS_21'], 'Access logging and object versioning are unnecessary for ephemeral chat transcripts', ); rc.set('buckets', 'StorySessions', { bucketName: storySessions.bucketName, });
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
dungeonDb.grantReadData(gameApi.integrations['games.query'].handler); dungeonDb.grantReadData(gameApi.integrations['inventory.query'].handler); dungeonDb.grantReadWriteData(gameApi.integrations['games.save'].handler); storySessions.grantRead(gameApi.integrations['actions.query'].handler);
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer'); dungeonDb.grantReadWriteData(mcpServer.agentCoreRuntime);
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { identity: userIdentity, }); storySessions.grantReadWrite(storyAgent);
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.agentRuntimeArn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.agentRuntimeArn, });
// Grant the agent permissions to invoke our mcp server mcpServer.grantInvokeAccess(storyAgent);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
new GameUI(this, 'GameUI'); }}import { DungeonDb, GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity, suppressRules,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Stack, StackProps, CfnOutput, RemovalPolicy } from 'aws-cdk-lib';import { BlockPublicAccess, Bucket, BucketEncryption,} from 'aws-cdk-lib/aws-s3';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const rc = RuntimeConfig.ensure(this);
const userIdentity = new UserIdentity(this, 'UserIdentity');
// Sandbox-friendly: allow the table to be deleted with the stack. const dungeonDb = new DungeonDb(this, 'DungeonDb', { deletionProtection: false, removalPolicy: RemovalPolicy.DESTROY, });
// S3 bucket for Strands conversation history. The Story Agent writes each // turn via ``S3SessionManager``; the Game API reads them back for replay. const storySessions = new Bucket(this, 'StorySessions', { encryption: BucketEncryption.S3_MANAGED, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, enforceSSL: true, removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, }); suppressRules( storySessions, ['CKV_AWS_18', 'CKV_AWS_21'], 'Access logging and object versioning are unnecessary for ephemeral chat transcripts', ); rc.set('buckets', 'StorySessions', { bucketName: storySessions.bucketName, });
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
dungeonDb.grantReadData(gameApi.integrations['games.query'].handler); dungeonDb.grantReadData(gameApi.integrations['inventory.query'].handler); dungeonDb.grantReadWriteData(gameApi.integrations['games.save'].handler); storySessions.grantRead(gameApi.integrations['actions.query'].handler);
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer'); dungeonDb.grantReadWriteData(mcpServer.agentCoreRuntime);
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { identity: userIdentity, }); storySessions.grantReadWrite(storyAgent);
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.agentRuntimeArn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.agentRuntimeArn, });
// Grant the agent permissions to invoke our mcp server mcpServer.grantInvokeAccess(storyAgent);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
new GameUI(this, 'GameUI'); }}Nhiệm vụ 4: Kiểm thử Game API locally
Phần tiêu đề “Nhiệm vụ 4: Kiểm thử Game API locally”Không cần triển khai lên AWS để thử API của chúng ta — target serve-local chạy Game API với DynamoDB Local. Vì chúng ta đã kết nối Game API với dự án DungeonDb trong Module 1, target này cũng tự động khởi động DynamoDB Local.
Đầu tiên, sửa mọi vấn đề lint:
pnpm lintyarn lintnpm run lintbun lintSau đó build codebase:
pnpm buildyarn buildnpm run buildbun buildKhởi động local server
Phần tiêu đề “Khởi động local server”Khởi động Game API locally với target dev, cũng khởi động DynamoDB Local:
pnpm nx dev game-apiyarn nx dev game-apinpx nx dev game-apibunx nx dev game-apiKiểm thử API
Phần tiêu đề “Kiểm thử API”Khi server của bạn đã chạy, truy vấn danh sách trò chơi (rỗng):
curl -X GET 'http://localhost:2022/games.query?input=%7B%7D'Bạn sẽ thấy một danh sách rỗng:
{"result":{"data":{"items":[],"cursor":null}}}Bây giờ lưu một trò chơi:
curl -X POST 'http://localhost:2022/games.save' \ -H 'Content-Type: application/json' \ -d '{"playerName":"Alice","genre":"zombie"}'Lệnh save trả về trò chơi đã được lưu (với timestamp lastUpdated mà entity đặt cho bạn):
{"result":{"data":{"playerName":"Alice","genre":"zombie","lastUpdated":"..."}}}Truy vấn lại để xác nhận nó đã được lưu trong DynamoDB Local:
curl -X GET 'http://localhost:2022/games.query?input=%7B%7D'Phản hồi này bây giờ bao gồm trò chơi đã lưu:
{"result":{"data":{"items":[{"playerName":"Alice","genre":"zombie","lastUpdated":"..."}],"cursor":null}}}Bạn có thể dừng local server (Ctrl+C) khi đã xong.
Nhiệm vụ 5: Kiểm thử Inventory MCP server locally
Phần tiêu đề “Nhiệm vụ 5: Kiểm thử Inventory MCP server locally”Chúng ta có thể thử các công cụ của MCP server với MCP Inspector bằng cách sử dụng target mcp-server-inspect được tạo:
pnpm nx mcp-server-inspect inventoryyarn nx mcp-server-inspect inventorynpx nx mcp-server-inspect inventorybunx nx mcp-server-inspect inventoryĐiều này phục vụ MCP server locally (cũng khởi động DynamoDB Local) và khởi chạy MCP Inspector tại http://localhost:6274 được cấu hình sẵn để kết nối với nó. Nhấp Connect, chuyển sang tab Tools, nhấp List Tools, và thử add-to-inventory (ví dụ: playerName: Alice, itemName: Rusty Sword, emoji: ⚔️) theo sau là list-inventory-items để xem nó được lưu vào DynamoDB Local. Dừng server (Ctrl+C) khi đã xong.
Chúc mừng, bạn đã xây dựng và kiểm thử API tRPC đầu tiên và MCP server với bảng DynamoDB local! 🎉🎉🎉