Used for feedback · quests · score card
Add a random facts tool to the MCP server. The agent discovers it automatically — no agent code changes needed.
A new MCP tool that returns fun facts
Agent finds the new tool with zero changes
Register, route, implement — that's it
First, add the new tool definition to the tools/list handler.
This tells the agent (and the LLM) what the tool does and what parameters it accepts.
The random facts tool takes an optional category parameter.
# Add to the tools list in handle_tools_list() { "name": "get_random_fact", "description": "Get a random fun fact. Optionally specify a category.", "inputSchema": { "type": "object", "properties": { "category": { "type": "string", "description": "Category: science, history, animals, technology, space", "enum": ["science", "history", "animals", "technology", "space"] } }, "required": [] } }
// Add to the tools list in HandleToolsList() new { name = "get_random_fact", description = "Get a random fun fact. Optionally specify a category.", inputSchema = new { type = "object", properties = new { category = new { type = "string", description = "Category: science, history, animals, technology, space", @enum = new[] { "science", "history", "animals", "technology", "space" } } }, required = Array.Empty<string>() } }
// Add to the tools array in handleToolsList() { name: 'get_random_fact', description: 'Get a random fun fact. Optionally specify a category.', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Category: science, history, animals, technology, space', enum: ['science', 'history', 'animals', 'technology', 'space'] } }, required: [] } }
// Add to the tools slice in HandleToolsList() Tool{ Name: "get_random_fact", Description: "Get a random fun fact. Optionally specify a category.", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "category": map[string]interface{}{ "type": "string", "description": "Category: science, history, animals, technology, space", "enum": []string{"science", "history", "animals", "technology", "space"}, }, }, "required": []string{}, }, }
// Add one entry to toolDefinitions(). Nothing else in the server changes, and // the agent picks the tool up on its next restart without any code change. new ToolDefinition( "get_random_fact", "Get a random fun fact. No input required.", InputSchema.of(Map.of())) // A tool that does take arguments looks like this. The last varargs parameter // lists the required property names. new ToolDefinition( "get_random_fact", "Get a random fun fact, optionally about a specific category.", InputSchema.of( Map.of("category", Property.string("Optional category: 'space', 'animals', 'history'"))))
Write the function that generates random facts. For this workshop we use a hardcoded list, but you could easily connect to an external API later.
import random FACTS = { "science": [ "Honey never spoils. Archaeologists have found 3000-year-old honey in Egyptian tombs that was still edible.", "A teaspoon of a neutron star would weigh about 6 billion tons.", "Octopuses have three hearts and blue blood.", ], "history": [ "Cleopatra lived closer in time to the Moon landing than to the construction of the Great Pyramid.", "The Oxford University is older than the Aztec Empire.", "Ancient Romans used crushed mouse brains as toothpaste.", ], "animals": [ "A group of flamingos is called a 'flamboyance'.", "Cows have best friends and get stressed when separated.", "Sea otters hold hands while sleeping so they don't drift apart.", ], "technology": [ "The first computer bug was an actual moth found in a Harvard Mark II computer in 1947.", "The entire Apollo 11 computer had less processing power than a modern calculator.", "The first domain name ever registered was symbolics.com on March 15, 1985.", ], "space": [ "There are more stars in the universe than grains of sand on all of Earth's beaches.", "A day on Venus is longer than a year on Venus.", "Neutron stars can spin at a rate of 600 rotations per second.", ], } def get_random_fact(category: str = None) -> str: if category and category in FACTS: return random.choice(FACTS[category]) all_facts = [f for facts in FACTS.values() for f in facts] return random.choice(all_facts)
public static class RandomFactTool { private static readonly Dictionary<string, string[]> Facts = new() { ["science"] = new[] { "Honey never spoils. Archaeologists found 3000-year-old honey still edible.", "A teaspoon of a neutron star would weigh about 6 billion tons.", "Octopuses have three hearts and blue blood.", }, ["history"] = new[] { "Cleopatra lived closer to the Moon landing than to the Great Pyramid.", "Oxford University is older than the Aztec Empire.", "Ancient Romans used crushed mouse brains as toothpaste.", }, ["animals"] = new[] { "A group of flamingos is called a 'flamboyance'.", "Cows have best friends and get stressed when separated.", "Sea otters hold hands while sleeping to avoid drifting apart.", }, ["technology"] = new[] { "The first computer bug was a real moth found in a Harvard Mark II in 1947.", "Apollo 11's computer had less power than a modern calculator.", "The first domain ever registered was symbolics.com in 1985.", }, ["space"] = new[] { "More stars exist than grains of sand on all Earth's beaches.", "A day on Venus is longer than a year on Venus.", "Neutron stars can spin at 600 rotations per second.", }, }; public static string GetRandomFact(string? category = null) { var rng = Random.Shared; if (category != null && Facts.TryGetValue(category, out var list)) return list[rng.Next(list.Length)]; var all = Facts.Values.SelectMany(f => f).ToArray(); return all[rng.Next(all.Length)]; } }
const FACTS: Record<string, string[]> = { science: [ 'Honey never spoils. 3000-year-old honey was found still edible.', 'A teaspoon of a neutron star would weigh about 6 billion tons.', 'Octopuses have three hearts and blue blood.', ], history: [ 'Cleopatra lived closer to the Moon landing than to the Great Pyramid.', 'Oxford University is older than the Aztec Empire.', 'Ancient Romans used crushed mouse brains as toothpaste.', ], animals: [ "A group of flamingos is called a 'flamboyance'.", 'Cows have best friends and get stressed when separated.', "Sea otters hold hands while sleeping to avoid drifting apart.", ], technology: [ 'The first computer bug was a real moth found in a Harvard Mark II in 1947.', "Apollo 11's computer had less power than a modern calculator.", 'The first domain ever registered was symbolics.com in 1985.', ], space: [ "More stars exist than grains of sand on all Earth's beaches.", 'A day on Venus is longer than a year on Venus.', 'Neutron stars can spin at 600 rotations per second.', ], }; export function getRandomFact(category?: string): string { if (category && FACTS[category]) { const list = FACTS[category]; return list[Math.floor(Math.random() * list.length)]; } const all = Object.values(FACTS).flat(); return all[Math.floor(Math.random() * all.length)]; }
package tools import "math/rand" var facts = map[string][]string{ "science": { "Honey never spoils. 3000-year-old honey was found still edible.", "A teaspoon of a neutron star would weigh about 6 billion tons.", "Octopuses have three hearts and blue blood.", }, "history": { "Cleopatra lived closer to the Moon landing than to the Great Pyramid.", "Oxford University is older than the Aztec Empire.", "Ancient Romans used crushed mouse brains as toothpaste.", }, "animals": { "A group of flamingos is called a 'flamboyance'.", "Cows have best friends and get stressed when separated.", "Sea otters hold hands while sleeping to avoid drifting apart.", }, "technology": { "The first computer bug was a real moth in a Harvard Mark II in 1947.", "Apollo 11's computer had less power than a modern calculator.", "The first domain ever registered was symbolics.com in 1985.", }, "space": { "More stars exist than grains of sand on all Earth's beaches.", "A day on Venus is longer than a year on Venus.", "Neutron stars can spin at 600 rotations per second.", }, } func GetRandomFact(category string) string { if list, ok := facts[category]; ok { return list[rand.Intn(len(list))] } var all []string for _, v := range facts { all = append(all, v...) } return all[rand.Intn(len(all))] }
package no.javazone.mcp; import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import org.springframework.stereotype.Component; // No arguments and no network call, which makes this the tool to reach for when // you are debugging: if this works and the weather tool does not, the problem is // upstream, not in your MCP plumbing. @Component class RandomFactTool { private static final Map<String, List<String>> FACTS = Map.of( "space", List.of( "A day on Venus is longer than a year on Venus.", "There are more possible iterations of a game of chess than atoms in the known universe."), "animals", List.of( "Octopuses have three hearts and blue blood.", "A group of flamingos is called a 'flamboyance'.", "The heart of a shrimp is located in its head."), "history", List.of( "Honey never spoils. Archaeologists have found 3000-year-old honey in Egyptian tombs.", "The shortest war in history lasted 38-45 minutes, between Britain and Zanzibar in 1896.", "Norway once knighted a penguin named Nils Olav.")); ToolResult call(Map<String, Object> arguments) { // An unknown category is not an error - fall back to everything rather // than making the model apologise for a guess it could not have known. var pool = arguments.get("category") instanceof String c && FACTS.containsKey(c) ? FACTS.get(c) : FACTS.values().stream().flatMap(List::stream).toList(); var fact = pool.get(ThreadLocalRandom.current().nextInt(pool.size())); return ToolResult.success(fact, Map.of("fact", fact)); } }
Add routing in the tools/call handler so when the agent
requests get_random_fact, the server calls your new function.
# In handle_tools_call(), add a new elif branch: elif tool_name == "get_random_fact": category = arguments.get("category") fact = get_random_fact(category) return { "content": [{"type": "text", "text": fact}], "isError": False }
// In HandleToolsCall(), add a new case: case "get_random_fact": var category = args.TryGetProperty("category", out var cat) ? cat.GetString() : null; var fact = RandomFactTool.GetRandomFact(category); return new { content = new[] { new { type = "text", text = fact } }, isError = false };
// In handleToolsCall(), add a new case: case 'get_random_fact': { const fact = getRandomFact(args.category); return { content: [{ type: 'text', text: fact }], isError: false }; }
// In handleToolsCall(), add a new case: case "get_random_fact": category, _ := args["category"].(string) fact := tools.GetRandomFact(category) return map[string]interface{}{ "content": []map[string]string{{"type": "text", "text": fact}}, "isError": false, }, nil
// Routing is a map lookup, not a switch. Spring injects each tool, and the // registry is the only place a tool name appears twice. private final Map<String, Function<Map<String, Object>, ToolResult>> registry; McpController(WeatherTool weather, RandomFactTool randomFact, NewsTool news) { this.registry = Map.of( "get_weather_forecast", weather::call, "get_random_fact", randomFact::call, "get_news", news::call); } private JsonRpcResponse callTool(JsonRpcRequest request) { var tool = registry.get(name); if (tool == null) { // -32602 is "invalid params" - the method existed, the tool name did not. return JsonRpcResponse.failure(request.id(), -32602, "Unknown tool: " + name); } return JsonRpcResponse.ok(request.id(), tool.apply(arguments)); }
Pattern: Adding a tool to the MCP server always follows the same three steps:
(1) register in tools/list,
(2) implement the function,
(3) route in tools/call.
The agent never needs to change.
Rebuild just the MCP server and restart. The agent will re-discover tools on startup and
automatically find get_random_fact.
# Rebuild the MCP server only docker compose build mcp-server # Restart all services (agent re-discovers tools on startup) docker compose up -d # Verify the new tool appears in tools/list curl -X POST http://localhost:8000/message \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/list" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
You should see get_random_fact in the tools list alongside
get_weather.
Call the tool directly via JSON-RPC to verify it works before testing through the agent.
# Call without a category (random from all) curl -X POST http://localhost:8000/message \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_random_fact" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_random_fact","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' # Call with a specific category curl -X POST http://localhost:8000/message \ -H "Content-Type: application/json" \ -H "MCP-Protocol-Version: 2026-07-28" \ -H "Mcp-Method: tools/call" \ -H "Mcp-Name: get_random_fact" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_random_fact","arguments":{"category":"space"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
Now ask the agent for a fact. The LLM will decide to use the new tool — no agent code changes were needed.
# Ask for a random fact curl -X POST http://localhost:8001/query \ -H "Content-Type: application/json" \ -d '{"query": "Tell me a fun fact about space"}' # Try combining tools in a single query curl -X POST http://localhost:8001/query \ -H "Content-Type: application/json" \ -d '{"query": "What is the weather in Oslo and tell me a random science fact"}'
This is the power of MCP: You added a tool to the server, and the agent automatically discovered and used it. The LLM decides when to call which tool based on the user's query. Try the combined query — the LLM will call both tools in one go.
get_random_fact tool with category support
Your feedback helps us improve the workshop.
Your handle is sent with this feedback; leave it blank in the header and the submission stays anonymous. Please keep personal data out of the comment too — no name, e-mail address or employer, yours or anyone else's.