Implementare l'API del Gioco e il server MCP dell'Inventario
Task 1: Implementare la Game API
Sezione intitolata “Task 1: Implementare la Game API”Implementeremo le seguenti API in questa sezione:
saveGame- crea o aggiorna un gioco.queryGames- restituisce una lista paginata di giochi salvati precedentemente.queryInventory- restituisce una lista paginata degli oggetti nell’inventario di un giocatore.queryActions- restituisce la cronologia delle conversazioni per un dato gioco.
Schema dell’API
Sezione intitolata “Schema dell’API”Per definire gli input e gli output della nostra API, creiamo il nostro schema utilizzando Zod nel file packages/game-api/src/schema/index.ts come segue:
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(), });};Elimina il file packages/game-api/src/schema/echo.ts poiché non lo utilizzeremo in questo progetto.
Modellazione delle entità
Sezione intitolata “Modellazione delle entità”Questo è il diagramma ER per la nostra applicazione.
Il generatore ts#dynamodb ha configurato ElectroDB, che utilizzeremo per modellare i nostri dati. Persisteremo la cronologia delle conversazioni in S3, quindi aggiungiamo una dipendenza sul client S3:
pnpm add -w @aws-sdk/client-s3@3.1075.0yarn add @aws-sdk/client-s3@3.1075.0npm install --legacy-peer-deps @aws-sdk/client-s3@3.1075.0bun install @aws-sdk/client-s3@3.1075.0Sostituisci l’entità di esempio generata in packages/dungeon-db/src/entities/index.ts con le nostre entità Game e Inventory, ed elimina 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 ci permette non solo di definire i nostri tipi, ma può anche fornire valori predefiniti per certi campi come i timestamp. Inoltre, ElectroDB segue il single-table design, che è la best practice quando si utilizza DynamoDB.
Definizione delle procedure
Sezione intitolata “Definizione delle procedure”Per implementare i metodi dell’API, apporta le seguenti modifiche in 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 }; });Elimina il file echo.ts (da packages/game-api/src/procedures) poiché non lo utilizzeremo in questo progetto.
Configurazione del router
Sezione intitolata “Configurazione del router”Dopo aver definito le nostre procedure, per collegarle alla nostra API, aggiorna il seguente file:
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;Task 2: Creare un server MCP per l’inventario
Sezione intitolata “Task 2: Creare un server MCP per l’inventario”Creiamo un server MCP che permetterà al nostro agente di gestire gli oggetti nell’inventario di un giocatore.
Definiremo i seguenti strumenti per il nostro agente:
list-inventory-itemsper recuperare gli oggetti correnti nell’inventario del giocatoreadd-to-inventoryper aggiungere oggetti all’inventario del giocatoreremove-from-inventoryper rimuovere oggetti dall’inventario del giocatore
Per risparmiare tempo, definiremo tutti gli strumenti 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;};Man mano che il numero di strumenti cresce, puoi eventualmente rifattorizzarli in file separati.
Elimina le directory tools e resources in packages/inventory/src/mcp-server poiché non verranno utilizzate.
Task 3: Aggiornare l’infrastruttura
Sezione intitolata “Task 3: Aggiornare l’infrastruttura”Il costrutto DungeonDb generato da ts#dynamodb fornisce già la nostra tabella, quindi dobbiamo solo istanziarlo nel nostro stack e concedere alla Game API e al server MCP dell’Inventario i permessi di cui hanno bisogno. Aggiorna packages/infra/src/stacks/application-stack.ts come segue:
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'); }}Task 4: Testare la Game API localmente
Sezione intitolata “Task 4: Testare la Game API localmente”Non c’è bisogno di deployare su AWS per provare la nostra API — il target dev esegue la Game API contro DynamoDB Local. Poiché abbiamo collegato la Game API al progetto DungeonDb nel Modulo 1, questo target avvia anche DynamoDB Local automaticamente.
Prima, correggi eventuali problemi di linting:
pnpm lintyarn lintnpm run lintbun lintPoi compila la codebase:
pnpm buildyarn buildnpm run buildbun buildAvviare il server locale
Sezione intitolata “Avviare il server locale”Avvia la Game API localmente con il target dev, che avvia anche DynamoDB Local:
pnpm nx dev game-apiyarn nx dev game-apinpx nx dev game-apibunx nx dev game-apiTestare l’API
Sezione intitolata “Testare l’API”Una volta avviato il server, interroga la lista (vuota) dei giochi:
curl -X GET 'http://localhost:2022/games.query?input=%7B%7D'Vedrai una lista vuota:
{"result":{"data":{"items":[],"cursor":null}}}Ora salva un gioco:
curl -X POST 'http://localhost:2022/games.save' \ -H 'Content-Type: application/json' \ -d '{"playerName":"Alice","genre":"zombie"}'Il salvataggio restituisce il gioco persistito (con il timestamp lastUpdated che l’entità imposta per te):
{"result":{"data":{"playerName":"Alice","genre":"zombie","lastUpdated":"..."}}}Interroga di nuovo per confermare che è stato persistito in DynamoDB Local:
curl -X GET 'http://localhost:2022/games.query?input=%7B%7D'Questa risposta ora include il gioco salvato:
{"result":{"data":{"items":[{"playerName":"Alice","genre":"zombie","lastUpdated":"..."}],"cursor":null}}}Puoi fermare il server locale (Ctrl+C) una volta terminato.
Task 5: Testare il server MCP dell’Inventario localmente
Sezione intitolata “Task 5: Testare il server MCP dell’Inventario localmente”Possiamo provare gli strumenti del server MCP con l’MCP Inspector utilizzando il target mcp-server-inspect generato:
pnpm nx mcp-server-inspect inventoryyarn nx mcp-server-inspect inventorynpx nx mcp-server-inspect inventorybunx nx mcp-server-inspect inventoryQuesto serve il server MCP localmente (avviando anche DynamoDB Local) e lancia l’MCP Inspector su http://localhost:6274 pre-configurato per connettersi ad esso. Fai clic su Connect, passa alla scheda Tools, fai clic su List Tools, e prova add-to-inventory (ad es. playerName: Alice, itemName: Rusty Sword, emoji: ⚔️) seguito da list-inventory-items per vederlo persistito in DynamoDB Local. Ferma il server (Ctrl+C) quando hai finito.
Complimenti, hai costruito e testato la tua prima API tRPC e il server MCP contro una tabella DynamoDB locale! 🎉🎉🎉🎉🎉