Skip to content

Commit 93e272f

Browse files
bokelleyclaude
andauthored
feat: expose eval, agent context, and validation tools on /mcp (#2007)
* feat: expose eval, agent context, and validation tools on /mcp endpoint Promotes 10 internal Addie tools to first-class MCP tools so external clients (Claude Code, Claude Desktop) can call them directly instead of routing through chat_with_addie. - Evaluation (4): probe_adcp_agent, evaluate_agent_quality, test_rfp_response, test_io_execution - Agent context (3): save_agent, list_saved_agents, remove_saved_agent - Validation (3): validate_json, get_schema, validate_adagents Member tools bridge MCPAuthContext → MemberContext per-request and require authentication (anonymous callers get isError: true). Stateless tools (schema/property) are created once at startup. Closes #2004 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add empty changeset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove unused imports from server.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d6ceca3 commit 93e272f

5 files changed

Lines changed: 537 additions & 12 deletions

File tree

.changeset/mcp-exposed-tools.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

server/src/mcp/exposed-tools.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* Exposed MCP Tools
3+
*
4+
* Internal Addie tools promoted to first-class MCP tools on the /mcp endpoint,
5+
* callable directly by external MCP clients (Claude Code, Claude Desktop, etc.).
6+
*
7+
* Three categories:
8+
* 1. Evaluation — agent testing and compliance (requires auth)
9+
* 2. Agent context — save/list/remove agent credentials (requires auth)
10+
* 3. Validation — schema and adagents.json validation (stateless, no auth)
11+
*/
12+
13+
import { createLogger } from '../logger.js';
14+
import { MEMBER_TOOLS, createMemberToolHandlers } from '../addie/mcp/member-tools.js';
15+
import { SCHEMA_TOOLS, createSchemaToolHandlers } from '../addie/mcp/schema-tools.js';
16+
import { PROPERTY_TOOLS, createPropertyToolHandlers } from '../addie/mcp/property-tools.js';
17+
import type { MemberContext } from '../addie/member-context.js';
18+
import type { MCPAuthContext } from './auth.js';
19+
20+
const logger = createLogger('mcp-exposed-tools');
21+
22+
// ── Tool name sets ──────────────────────────────────────────────────
23+
24+
/** Agent evaluation tools (require auth for saved-agent credential lookup). */
25+
const EVAL_TOOL_NAMES = [
26+
'probe_adcp_agent',
27+
'evaluate_agent_quality',
28+
'test_rfp_response',
29+
'test_io_execution',
30+
] as const;
31+
32+
/** Agent context management tools (require auth). */
33+
const AGENT_CONTEXT_TOOL_NAMES = [
34+
'save_agent',
35+
'list_saved_agents',
36+
'remove_saved_agent',
37+
] as const;
38+
39+
/** Schema validation tools (stateless, no auth required). */
40+
const SCHEMA_TOOL_NAMES = [
41+
'validate_json',
42+
'get_schema',
43+
] as const;
44+
45+
/** Property validation tools (stateless, no auth required). */
46+
const PROPERTY_TOOL_NAMES = [
47+
'validate_adagents',
48+
] as const;
49+
50+
// ── Startup validation ──────────────────────────────────────────────
51+
// Fail at import time if any exposed tool name was renamed or removed upstream.
52+
53+
const memberToolNames = new Set(MEMBER_TOOLS.map((t) => t.name));
54+
for (const name of [...EVAL_TOOL_NAMES, ...AGENT_CONTEXT_TOOL_NAMES]) {
55+
if (!memberToolNames.has(name)) {
56+
throw new Error(`Exposed tool "${name}" not found in MEMBER_TOOLS — was it renamed or removed?`);
57+
}
58+
}
59+
60+
const schemaToolNames = new Set(SCHEMA_TOOLS.map((t) => t.name));
61+
for (const name of SCHEMA_TOOL_NAMES) {
62+
if (!schemaToolNames.has(name)) {
63+
throw new Error(`Exposed tool "${name}" not found in SCHEMA_TOOLS — was it renamed or removed?`);
64+
}
65+
}
66+
67+
const propertyToolNames = new Set(PROPERTY_TOOLS.map((t) => t.name));
68+
for (const name of PROPERTY_TOOL_NAMES) {
69+
if (!propertyToolNames.has(name)) {
70+
throw new Error(`Exposed tool "${name}" not found in PROPERTY_TOOLS — was it renamed or removed?`);
71+
}
72+
}
73+
74+
// ── Tool definitions (MCP format) ───────────────────────────────────
75+
76+
// usage_hints are intentionally excluded — they're for Addie's internal router,
77+
// not for external MCP clients.
78+
function toMCPFormat(tool: { name: string; description: string; input_schema: object }) {
79+
return { name: tool.name, description: tool.description, inputSchema: tool.input_schema };
80+
}
81+
82+
/** Evaluation tool definitions in MCP format. */
83+
export const EVAL_TOOL_DEFINITIONS = MEMBER_TOOLS
84+
.filter((t) => (EVAL_TOOL_NAMES as readonly string[]).includes(t.name))
85+
.map(toMCPFormat);
86+
87+
/** Agent context tool definitions in MCP format. */
88+
export const AGENT_CONTEXT_TOOL_DEFINITIONS = MEMBER_TOOLS
89+
.filter((t) => (AGENT_CONTEXT_TOOL_NAMES as readonly string[]).includes(t.name))
90+
.map(toMCPFormat);
91+
92+
/** Schema validation tool definitions in MCP format. */
93+
export const SCHEMA_TOOL_DEFINITIONS = SCHEMA_TOOLS
94+
.filter((t) => (SCHEMA_TOOL_NAMES as readonly string[]).includes(t.name))
95+
.map(toMCPFormat);
96+
97+
/** Property validation tool definitions in MCP format. */
98+
export const PROPERTY_TOOL_DEFINITIONS = PROPERTY_TOOLS
99+
.filter((t) => (PROPERTY_TOOL_NAMES as readonly string[]).includes(t.name))
100+
.map(toMCPFormat);
101+
102+
/** All exposed tool definitions combined. */
103+
export const ALL_EXPOSED_TOOL_DEFINITIONS = [
104+
...EVAL_TOOL_DEFINITIONS,
105+
...AGENT_CONTEXT_TOOL_DEFINITIONS,
106+
...SCHEMA_TOOL_DEFINITIONS,
107+
...PROPERTY_TOOL_DEFINITIONS,
108+
];
109+
110+
// ── Auth bridging ───────────────────────────────────────────────────
111+
112+
/**
113+
* Build a minimal MemberContext from MCP auth claims.
114+
*
115+
* Identity is verified via OAuth JWT, but membership/subscription status
116+
* is not resolved from the database. Tools gate on orgId presence for
117+
* credential lookup rather than membership status.
118+
*/
119+
function mcpAuthToMemberContext(auth: MCPAuthContext): MemberContext {
120+
return {
121+
is_mapped: true,
122+
is_member: false,
123+
workos_user: {
124+
workos_user_id: auth.sub,
125+
email: auth.email || '',
126+
},
127+
organization: auth.orgId
128+
? {
129+
workos_organization_id: auth.orgId,
130+
name: '',
131+
subscription_status: null,
132+
is_personal: false,
133+
}
134+
: undefined,
135+
} as MemberContext;
136+
}
137+
138+
// ── Handler factories ───────────────────────────────────────────────
139+
140+
/**
141+
* Create a handler for a member tool (eval or agent context) that bridges
142+
* MCPAuthContext to MemberContext. Requires authentication — anonymous
143+
* callers receive an error with isError: true.
144+
*/
145+
export function createMemberToolHandler(toolName: string) {
146+
return async (
147+
args: Record<string, unknown>,
148+
authContext?: MCPAuthContext,
149+
): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> => {
150+
if (!authContext || authContext.sub === 'anonymous') {
151+
return {
152+
content: [{ type: 'text', text: 'Authentication required. Connect with OAuth to use this tool.' }],
153+
isError: true,
154+
};
155+
}
156+
157+
const memberContext = mcpAuthToMemberContext(authContext);
158+
const handlers = createMemberToolHandlers(memberContext);
159+
const handler = handlers.get(toolName);
160+
161+
if (!handler) {
162+
logger.error({ toolName }, 'Member tool handler not found');
163+
return {
164+
content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${toolName}` }) }],
165+
isError: true,
166+
};
167+
}
168+
169+
const result = await handler(args);
170+
return { content: [{ type: 'text', text: result }] };
171+
};
172+
}
173+
174+
/**
175+
* Create handlers for stateless tools (schema and property validation).
176+
* These are created once at startup since they don't need per-request auth.
177+
*/
178+
export function createStatelessToolHandlers(): Map<
179+
string,
180+
(args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>
181+
> {
182+
const result = new Map<
183+
string,
184+
(args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>
185+
>();
186+
187+
const schemaHandlers = createSchemaToolHandlers();
188+
for (const name of SCHEMA_TOOL_NAMES) {
189+
const handler = schemaHandlers.get(name);
190+
if (handler) {
191+
result.set(name, async (args) => {
192+
const text = await handler(args);
193+
return { content: [{ type: 'text', text }] };
194+
});
195+
}
196+
}
197+
198+
const propertyHandlers = createPropertyToolHandlers();
199+
for (const name of PROPERTY_TOOL_NAMES) {
200+
const handler = propertyHandlers.get(name);
201+
if (handler) {
202+
result.set(name, async (args) => {
203+
const text = await handler(args);
204+
return { content: [{ type: 'text', text }] };
205+
});
206+
}
207+
}
208+
209+
return result;
210+
}

server/src/mcp/server.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* Public MCP interface exposing:
55
* - chat_with_addie: Conversational AI (wraps knowledge + directory tools internally)
66
* - Directory tools: Programmatic lookup (list_members, list_agents, etc.)
7+
* - Evaluation tools: Agent testing (probe, compliance, RFP response, IO execution)
8+
* - Agent context tools: Save/list/remove agent credentials
9+
* - Validation tools: Schema validation and adagents.json checking
710
*
811
* Knowledge and billing tools are NOT exposed directly - they're available
912
* through chat_with_addie for conversational access, or internal Slack use only.
@@ -38,6 +41,15 @@ import { MCPToolHandler, TOOL_DEFINITIONS, RESOURCE_DEFINITIONS } from '../mcp-t
3841
// Chat tool - conversational AI wrapper (has knowledge + directory tools internally)
3942
import { CHAT_TOOL, createChatToolHandler } from './chat-tool.js';
4043

44+
// Exposed tools - internal tools promoted to first-class MCP tools
45+
import {
46+
ALL_EXPOSED_TOOL_DEFINITIONS,
47+
EVAL_TOOL_DEFINITIONS,
48+
AGENT_CONTEXT_TOOL_DEFINITIONS,
49+
createMemberToolHandler,
50+
createStatelessToolHandlers,
51+
} from './exposed-tools.js';
52+
4153
const logger = createLogger('mcp-server');
4254

4355
/**
@@ -54,31 +66,37 @@ function convertToMCPTool(tool: AddieTool) {
5466
/**
5567
* All tools available in the unified MCP server
5668
*
57-
* Only exposes:
69+
* Exposes:
5870
* - chat_with_addie: Conversational wrapper (uses knowledge + directory tools internally)
5971
* - Directory tools: Programmatic member/agent/publisher lookup
72+
* - Evaluation tools: Agent testing (probe, compliance, RFP, IO execution)
73+
* - Agent context tools: Save/list/remove agent credentials
74+
* - Validation tools: Schema validation and adagents.json checking
6075
*
6176
* Knowledge and billing tools are NOT exposed - use chat_with_addie instead.
6277
*/
6378
export function getAllTools() {
6479
const chatTool = convertToMCPTool(CHAT_TOOL);
65-
66-
// Directory tools are already in MCP format
6780
const directoryTools = TOOL_DEFINITIONS;
81+
const exposedTools = ALL_EXPOSED_TOOL_DEFINITIONS;
6882

6983
return {
7084
directory: directoryTools,
85+
exposed: exposedTools,
7186
chat: chatTool,
72-
all: [chatTool, ...directoryTools],
87+
all: [chatTool, ...directoryTools, ...exposedTools],
7388
};
7489
}
7590

7691
/**
7792
* Create all tool handlers
7893
*
79-
* Only creates handlers for publicly exposed tools:
94+
* Creates handlers for publicly exposed tools:
8095
* - chat_with_addie
8196
* - Directory tools (list_members, list_agents, etc.)
97+
* - Evaluation tools (probe, compliance, RFP, IO execution)
98+
* - Agent context tools (save_agent, list_saved_agents, remove_saved_agent)
99+
* - Validation tools (validate_json, get_schema, validate_adagents)
82100
*/
83101
function createAllHandlers() {
84102
const handlers = new Map<string, (args: Record<string, unknown>, authContext?: MCPAuthContext) => Promise<unknown>>();
@@ -100,6 +118,18 @@ function createAllHandlers() {
100118
});
101119
}
102120

121+
// Member tools (eval + agent context) — need per-request auth bridging
122+
const memberToolDefs = [...EVAL_TOOL_DEFINITIONS, ...AGENT_CONTEXT_TOOL_DEFINITIONS];
123+
for (const tool of memberToolDefs) {
124+
handlers.set(tool.name, createMemberToolHandler(tool.name));
125+
}
126+
127+
// Stateless tools (schema + property validation) — created once
128+
const statelessHandlers = createStatelessToolHandlers();
129+
for (const [name, handler] of statelessHandlers) {
130+
handlers.set(name, async (args) => handler(args));
131+
}
132+
103133
return { handlers, directoryHandler };
104134
}
105135

@@ -124,6 +154,9 @@ function getHandlers() {
124154
* This server exposes Addie capabilities via MCP:
125155
* - chat_with_addie: Conversational AI with knowledge + directory access
126156
* - Directory tools: Programmatic lookup of members, agents, publishers
157+
* - Evaluation tools: Agent testing and compliance checking
158+
* - Agent context tools: Credential management for agent testing
159+
* - Validation tools: Schema and adagents.json validation
127160
*/
128161
export function createUnifiedMCPServer(authContext?: MCPAuthContext): Server {
129162
const server = new Server(

0 commit comments

Comments
 (0)