{"slug": "mcp-server-tutorial-build-your-own-ai-tools-in-30-minutes", "title": "MCP Server Tutorial: Build Your Own AI Tools in 30 Minutes", "summary": "A developer built an MCP server with three custom AI tools in 30 minutes, including a database query tool, a notification tool, and a file operations tool. The server uses Zod-validated tool schemas, rate limiting, and circuit breaker resilience, integrated with the NeuroLink SDK for end-to-end AI tool calling.", "body_md": "You will build an MCP server with three custom AI tools in 30 minutes: a database query tool, a notification tool, and a file operations tool. By the end of this tutorial, you will have a working MCP server with Zod-validated tool schemas, rate limiting, circuit breaker resilience, and full integration with the NeuroLink SDK for end-to-end AI tool calling.\n\nThe Model Context Protocol (MCP) decouples your business logic from your AI orchestration layer. Instead of hardcoding tool logic into your application, you define tools on a server that any AI agent can discover and execute at runtime. Now you will set up the server and build your first tool.\n\nMCP standardizes how AI models discover and execute tools. The protocol defines a clear lifecycle: a server registers tools with their schemas, an AI agent discovers those tools at connection time, the model decides when to call a tool based on the user's request, the server executes the tool logic, and the result flows back to the model for incorporation into the final response.\n\n```\nsequenceDiagram\n    participant U as User\n    participant A as AI Agent\n    participant M as MCP Server\n    participant T as Tool Implementation\n\n    U->>A: Ask question\n    A->>M: Discover available tools\n    M-->>A: Tool list with schemas\n    A->>A: LLM decides to call tool\n    A->>M: Execute tool with params\n    M->>T: Run tool logic\n    T-->>M: Return result\n    M-->>A: Tool result\n    A-->>U: Final answer using tool data\n```\n\nThe key insight is separation of concerns. Your MCP server encapsulates business logic -- database queries, API calls, file operations -- behind a clean tool interface. The AI agent does not need to know how the database works or how notifications are sent. It just calls the tool with the parameters the schema defines.\n\nThis pattern has several practical benefits:\n\nStart by creating a server using the `createMCPServer()`\n\nfactory function. The server needs an ID, title, description, and category.\n\n``` js\nimport { createMCPServer } from \"@juspay/neurolink\";\n\nconst server = createMCPServer({\n  id: \"my-business-tools\",\n  title: \"Business Tools Server\",\n  description: \"Custom tools for business operations\",\n  category: \"business\",\n  version: \"1.0.0\",\n});\n\nconsole.log(\"Server created:\", server.id);\nconsole.log(\"Category:\", server.category); // \"business\"\n```\n\nThe `category`\n\nfield classifies your server for discovery and organization. NeuroLink supports the following categories: `aiProviders`\n\n, `frameworks`\n\n, `development`\n\n, `business`\n\n, `content`\n\n, `data`\n\n, `integrations`\n\n, `automation`\n\n, `analysis`\n\n, and `custom`\n\n. Choose the one that best describes your tools' purpose.\n\nThe server object is a lightweight container that holds tool registrations and provides methods for validation and execution. It does not start an HTTP server or listen on a port. It is a logical grouping of tools that can be embedded in any application, exposed over HTTP, or used directly in-process.\n\nTools are the core of your MCP server. Each tool needs a `name`\n\n, a `description`\n\n(used by the LLM to decide when to call it), a `parameters`\n\nschema (defined with Zod for runtime validation and type inference), and an `execute`\n\nfunction that contains your business logic.\n\n``` js\nimport { z } from \"zod\";\n\n// Tool 1: Database query tool\nserver.registerTool({\n  name: \"queryDatabase\",\n  description: \"Execute a read-only SQL query against the analytics database\",\n  parameters: z.object({\n    query: z.string().describe(\"SQL SELECT query\"),\n    limit: z.number().optional().default(100).describe(\"Max rows\"),\n  }),\n  execute: async (params) => {\n    const { query, limit } = params;\n    if (!query.trim().toUpperCase().startsWith(\"SELECT\")) {\n      return { success: false, error: \"Only SELECT queries allowed\" };\n    }\n    // Use a read-only database role for defense-in-depth\n    const results = await db.query(`${query} LIMIT $1`, [limit]);\n    return { success: true, data: results, rowCount: results.length };\n  },\n});\n\n// Tool 2: Send notification tool\nserver.registerTool({\n  name: \"sendNotification\",\n  description: \"Send a notification to a Slack channel or email\",\n  parameters: z.object({\n    channel: z.enum([\"slack\", \"email\"]).describe(\"Notification channel\"),\n    recipient: z.string().describe(\"Channel ID or email address\"),\n    message: z.string().describe(\"Notification message\"),\n  }),\n  execute: async (params) => {\n    if (params.channel === \"slack\") {\n      await slackClient.postMessage(params.recipient, params.message);\n    } else {\n      await emailClient.send(params.recipient, \"AI Notification\", params.message);\n    }\n    return { success: true, channel: params.channel };\n  },\n});\n\n// Tool 3: File operations\nserver.registerTool({\n  name: \"readFile\",\n  description: \"Read the contents of a file from the project directory\",\n  parameters: z.object({\n    path: z.string().describe(\"Relative file path\"),\n  }),\n  execute: async (params) => {\n    const PROJECT_DIR = path.resolve(process.cwd());\n    const safePath = path.resolve(PROJECT_DIR, params.path);\n    if (!safePath.startsWith(PROJECT_DIR)) {\n      return { success: false, error: \"Path traversal detected\" };\n    }\n    const content = await fs.readFile(safePath, \"utf-8\");\n    return { success: true, content, size: content.length };\n  },\n});\n```\n\nSecurity:This example allows the LLM to submit arbitrary SELECT queries. In production, use a read-only database role, restrict queries to an allowlist of approved tables, and consider a query builder like Knex or Drizzle instead of raw SQL. The`startsWith(\"SELECT\")`\n\ncheck is a minimal guard — it does not prevent data exfiltration via`UNION`\n\nor subqueries. Always use parameterized queries for user-supplied values (like`limit`\n\n), and never interpolate untrusted input into SQL identifiers (table or column names).\n\n{: .prompt-warning }\n\nA few important design principles for tool definitions:\n\n**Descriptions matter more than names.** The LLM reads the description to decide when to call the tool. Write descriptions that clearly state what the tool does, what inputs it expects, and what it returns. A vague description leads to incorrect tool selection.\n\n**Zod schemas enforce contracts.** The `parameters`\n\nschema defines the exact shape of the input the tool accepts. Zod validates inputs at runtime, so malformed parameters from the LLM are caught before your business logic runs. Use `.describe()`\n\non each field to give the LLM hints about expected values.\n\n**Execute functions should be defensive.** Always validate inputs beyond what Zod checks. In the database tool example, we verify the query starts with SELECT even though the description says \"read-only\" -- because LLMs do not always follow instructions perfectly.\n\nNote:Tool names should be camelCase and descriptive. The LLM uses the name alongside the description to determine when a tool is appropriate. Avoid generic names like \"doThing\" or \"process\" -- specific names like \"queryDatabase\" or \"sendNotification\" give the model clearer intent signals.\n\n{: .prompt-info }\n\nBefore using your tools in production, validate them to ensure they follow proper patterns. The `validateServerTools()`\n\nfunction checks all registered tools for completeness and correctness.\n\n``` js\nimport { validateServerTools, getServerInfo } from \"@juspay/neurolink\";\n\n// Validate all tools\nconst validation = await validateServerTools(server);\n\nif (!validation.isValid) {\n  console.error(\"Invalid tools:\", validation.invalidTools);\n  console.error(\"Errors:\", validation.errors);\n  process.exit(1);\n}\n\n// Get server info\nconst info = getServerInfo(server);\nconsole.log(`Server: ${info.title}`);\nconsole.log(`Tools registered: ${info.toolCount}`);\nconsole.log(`Category: ${info.category}`);\n```\n\nValidation checks include: tool names are non-empty strings, descriptions exist and are meaningful, execute functions are callable, and parameter schemas are valid Zod objects. Running validation at startup catches configuration errors early, before any user request hits a broken tool.\n\nThe `getServerInfo()`\n\nfunction provides a summary of the server's state: how many tools are registered, what category it belongs to, and its version. This is useful for health check endpoints and operational dashboards.\n\nNow connect your MCP tools to the NeuroLink SDK so that LLMs can discover and call them during generation.\n\n``` js\nimport { NeuroLink } from \"@juspay/neurolink\";\nimport { tool } from \"ai\";\nimport { z } from \"zod\";\n\nconst neurolink = new NeuroLink();\n\n// Convert MCP tools to AI SDK format for use with generate/stream\nconst aiTools = {\n  queryDatabase: tool({\n    description: \"Execute a read-only SQL query against the analytics database\",\n    parameters: z.object({\n      query: z.string().describe(\"SQL SELECT query\"),\n      limit: z.number().optional().default(100),\n    }),\n    execute: async (params) => {\n      // Delegate to MCP server tool\n      return server.tools[\"queryDatabase\"].execute(params);\n    },\n  }),\n  sendNotification: tool({\n    description: \"Send a notification to a Slack channel or email\",\n    parameters: z.object({\n      channel: z.enum([\"slack\", \"email\"]),\n      recipient: z.string(),\n      message: z.string(),\n    }),\n    execute: async (params) => {\n      return server.tools[\"sendNotification\"].execute(params);\n    },\n  }),\n};\n\n// Use tools in generation\nconst result = await neurolink.generate({\n  input: {\n    text: \"How many orders did we process last week? Send a summary to #analytics on Slack.\",\n  },\n  provider: \"openai\",\n  model: \"gpt-4o\",\n  tools: aiTools,\n});\n\nconsole.log(result.content);\n```\n\nWhen you pass tools to `neurolink.generate()`\n\n, the LLM receives the tool schemas as part of its system context. It then decides whether to call tools based on the user's request. In this example, the model would likely call `queryDatabase`\n\nto get order counts, then call `sendNotification`\n\nto post the summary to Slack, and finally synthesize a natural language response.\n\nThe delegation pattern (AI tool wrapping MCP server tool) keeps your MCP server as the single source of truth for tool logic. The AI SDK tools are thin wrappers that forward execution to the MCP server. This means you can update tool logic in one place and all consumers get the update automatically.\n\nNote:The`tools`\n\noption in`generate()`\n\naccepts tools in the AI SDK format. The MCP server's`registerTool()`\n\nuses a slightly different shape. The wrapper pattern shown above bridges the two formats cleanly. In the future, NeuroLink will support direct MCP tool passthrough.\n\n{: .prompt-info }\n\nProduction MCP servers need protection against abuse and cascading failures. NeuroLink provides built-in rate limiting and circuit breaking specifically designed for MCP tool execution.\n\n``` js\nimport {\n  HTTPRateLimiter,\n  MCPCircuitBreaker,\n  DEFAULT_RATE_LIMIT_CONFIG,\n} from \"@juspay/neurolink\";\n\n// Rate limit: 100 requests per minute\nconst rateLimiter = new HTTPRateLimiter({\n  ...DEFAULT_RATE_LIMIT_CONFIG,\n  maxRequests: 100,\n  windowMs: 60000,\n});\n\n// Circuit breaker: Open after 5 failures, reset after 30s\nconst circuitBreaker = new MCPCircuitBreaker({\n  failureThreshold: 5,\n  resetTimeoutMs: 30000,\n});\n```\n\nThe rate limiter prevents any single client from overwhelming your tools. At 100 requests per minute, a runaway agent loop would be throttled before it racks up significant costs or overwhelms your database.\n\nThe circuit breaker monitors failure rates for tool execution. After five consecutive failures (a database connection timeout, an API outage, etc.), the circuit opens and immediately returns errors without attempting execution. After 30 seconds, the circuit enters a half-open state and allows a single test request through. If it succeeds, the circuit closes and normal operation resumes. If it fails, the circuit stays open for another 30 seconds.\n\nTogether, rate limiting and circuit breaking give your MCP server production-grade resilience without complex custom implementation.\n\nThe `validateTool()`\n\nfunction provides fine-grained validation for individual tools, useful during development and testing:\n\n``` js\nimport { validateTool } from \"@juspay/neurolink\";\n\nconst isValid = validateTool({\n  name: \"myTool\",\n  description: \"Does something useful\",\n  execute: async (params) => ({ result: \"ok\" }),\n});\n\nconsole.log(\"Valid:\", isValid); // true\n```\n\nValidation checks cover several categories:\n\nRunning validation in your CI/CD pipeline ensures that no malformed tool definitions ship to production.\n\nHere is the complete architecture of an MCP server integrated with NeuroLink:\n\n``` php\nflowchart TD\n    A[createMCPServer] --> B[MCP Server Instance]\n    B --> C[registerTool x3]\n    C --> D[validateServerTools]\n    D --> E{Valid?}\n    E -->|Yes| F[NeuroLink SDK]\n    E -->|No| G[Fix Errors]\n    G --> C\n    F --> H[generate/stream with tools]\n    H --> I[LLM calls tools]\n    I --> J[Tool executes]\n    J --> K[Result returned to LLM]\n    K --> L[Final response]\n\n    M[Rate Limiter] --> I\n    N[Circuit Breaker] --> I\n```\n\nThe flow is straightforward: create a server, register tools, validate them, and connect to NeuroLink. During generation, the LLM calls tools as needed, with rate limiting and circuit breaking protecting every execution. Results flow back to the LLM for synthesis into a final response.\n\nTestability is one of the strongest benefits of the MCP pattern. Because tools have defined inputs and outputs, you can unit test them without any AI involvement:\n\n``` js\n// Unit test\nconst result = await server.tools[\"queryDatabase\"].execute({\n  query: \"SELECT COUNT(*) FROM orders WHERE date > '2025-01-01'\",\n  limit: 1,\n});\nassert(result.success === true);\n```\n\nFor integration testing, run the full flow through `neurolink.generate()`\n\nwith your tools and verify that the model correctly identifies when to call each tool and how to interpret the results. Mock your external dependencies (database, Slack, email) to keep integration tests fast and deterministic.\n\nTest edge cases thoroughly: What happens when the database returns zero rows? When the Slack API is down? When the file does not exist? Each tool should return structured error responses that the LLM can interpret gracefully, rather than throwing unhandled exceptions.\n\n``` js\n// Edge case test: invalid SQL\nconst invalidResult = await server.tools[\"queryDatabase\"].execute({\n  query: \"DROP TABLE orders\",\n  limit: 1,\n});\nassert(invalidResult.success === false);\nassert(invalidResult.error === \"Only SELECT queries allowed\");\n\n// Edge case test: missing file\ntry {\n  await server.tools[\"readFile\"].execute({\n    path: \"./nonexistent.txt\",\n  });\n  assert.fail(\"Should have thrown\");\n} catch (error) {\n  assert(error.code === \"ENOENT\");\n}\n```\n\nTip:Always return structured error objects from your tools rather than throwing exceptions. The LLM can interpret a`{ success: false, error: \"...\" }`\n\nresponse and adjust its approach, but an unhandled exception terminates the tool call chain entirely.\n\n{: .prompt-tip }\n\nBeyond the basics, here are patterns we see in production MCP deployments:\n\n**Composite tools** wrap multiple operations into a single tool call. Instead of the LLM calling \"queryDatabase\" and then \"sendNotification\" separately, a \"generateAndSendReport\" tool handles the entire workflow internally. This reduces the number of tool calls and the chance of the LLM making intermediate mistakes.\n\n**Parameterized permissions** restrict tool access based on the calling context. A tool can check the user's role before executing sensitive operations, returning a permission error if the caller lacks the required access level.\n\n```\n// Authentication middleware for MCP tool execution\nfunction withAuth(tool: MCPTool, requiredRole: string): MCPTool {\n  return {\n    ...tool,\n    execute: async (params, context) => {\n      const user = await verifyToken(context.headers?.authorization);\n      if (!user || !user.roles.includes(requiredRole)) {\n        return { success: false, error: \"Unauthorized: insufficient permissions\" };\n      }\n      return tool.execute(params, { ...context, user });\n    },\n  };\n}\n\n// Usage\nserver.registerTool(withAuth(queryDatabaseTool, \"analyst\"));\nserver.registerTool(withAuth(sendNotificationTool, \"admin\"));\n```\n\n**Caching layers** store frequent tool results for reuse. If ten users ask \"How many orders this month?\" in a minute, the database tool can serve cached results instead of hitting the database ten times.\n\n**Audit logging** records every tool call with its parameters, caller, timestamp, and result. This is essential for regulated industries where you need to prove what the AI did and why.\n\nYou built a fully functional MCP server with three validated tools, rate limiting, circuit breaker resilience, and end-to-end NeuroLink integration. Your tools are discoverable, testable, and usable by any AI agent without code changes.\n\nContinue with these related tutorials:\n\n**Related posts:**", "url": "https://wpnews.pro/news/mcp-server-tutorial-build-your-own-ai-tools-in-30-minutes", "canonical_source": "https://dev.to/neurolink/mcp-server-tutorial-build-your-own-ai-tools-in-30-minutes-3nfe", "published_at": "2026-07-04 04:02:39+00:00", "updated_at": "2026-07-04 04:19:12.809525+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models", "ai-agents"], "entities": ["NeuroLink SDK", "Zod", "MCP", "Juspay"], "alternates": {"html": "https://wpnews.pro/news/mcp-server-tutorial-build-your-own-ai-tools-in-30-minutes", "markdown": "https://wpnews.pro/news/mcp-server-tutorial-build-your-own-ai-tools-in-30-minutes.md", "text": "https://wpnews.pro/news/mcp-server-tutorial-build-your-own-ai-tools-in-30-minutes.txt", "jsonld": "https://wpnews.pro/news/mcp-server-tutorial-build-your-own-ai-tools-in-30-minutes.jsonld"}}