tools.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import type {
  2. Prompt,
  3. Resource,
  4. Tool,
  5. } from '@modelcontextprotocol/sdk/types.js';
  6. import { type ToolDefinition, tool } from '@opencode-ai/plugin';
  7. import type { PluginConfig } from '../../config/schema';
  8. import {
  9. canAgentUseMcp,
  10. canAgentUseSkill,
  11. getBuiltinSkills,
  12. getSkillByName,
  13. getSkillsForAgent,
  14. } from './builtin';
  15. import {
  16. SKILL_LIST_TOOL_DESCRIPTION,
  17. SKILL_MCP_TOOL_DESCRIPTION,
  18. SKILL_TOOL_DESCRIPTION,
  19. } from './constants';
  20. import type { SkillMcpManager } from './mcp-manager';
  21. import type { SkillArgs, SkillDefinition, SkillMcpArgs } from './types';
  22. type ToolContext = {
  23. sessionID: string;
  24. messageID: string;
  25. agent: string;
  26. abort: AbortSignal;
  27. };
  28. function formatSkillsList(skills: SkillDefinition[]): string {
  29. if (skills.length === 0) return 'No skills available for this agent.';
  30. return skills
  31. .map((skill) => `- ${skill.name}: ${skill.description}`)
  32. .join('\n');
  33. }
  34. async function formatMcpCapabilities(
  35. skill: SkillDefinition,
  36. manager: SkillMcpManager,
  37. sessionId: string,
  38. agentName: string,
  39. pluginConfig?: PluginConfig,
  40. ): Promise<string | null> {
  41. if (!skill.mcpConfig || Object.keys(skill.mcpConfig).length === 0) {
  42. return null;
  43. }
  44. const sections: string[] = ['', '## Available MCP Servers', ''];
  45. for (const [serverName, config] of Object.entries(skill.mcpConfig)) {
  46. // Check if this agent can use this MCP
  47. if (!canAgentUseMcp(agentName, serverName, pluginConfig)) {
  48. continue; // Skip this MCP - agent doesn't have permission
  49. }
  50. const info = {
  51. serverName,
  52. skillName: skill.name,
  53. sessionId,
  54. };
  55. sections.push(`### ${serverName}`);
  56. sections.push('');
  57. try {
  58. const [tools, resources, prompts] = await Promise.all([
  59. manager.listTools(info, config).catch(() => []),
  60. manager.listResources(info, config).catch(() => []),
  61. manager.listPrompts(info, config).catch(() => []),
  62. ]);
  63. if (tools.length > 0) {
  64. sections.push('**Tools:**');
  65. sections.push('');
  66. for (const t of tools as Tool[]) {
  67. sections.push(`#### \`${t.name}\``);
  68. if (t.description) {
  69. sections.push(t.description);
  70. }
  71. sections.push('');
  72. sections.push('**inputSchema:**');
  73. sections.push('```json');
  74. sections.push(JSON.stringify(t.inputSchema, null, 2));
  75. sections.push('```');
  76. sections.push('');
  77. }
  78. }
  79. if (resources.length > 0) {
  80. sections.push(
  81. `**Resources**: ${(resources as Resource[])
  82. .map((r) => r.uri)
  83. .join(', ')}`,
  84. );
  85. }
  86. if (prompts.length > 0) {
  87. sections.push(
  88. `**Prompts**: ${(prompts as Prompt[]).map((p) => p.name).join(', ')}`,
  89. );
  90. }
  91. if (
  92. tools.length === 0 &&
  93. resources.length === 0 &&
  94. prompts.length === 0
  95. ) {
  96. sections.push('*No capabilities discovered*');
  97. }
  98. } catch (error) {
  99. const errorMessage =
  100. error instanceof Error ? error.message : String(error);
  101. sections.push(`*Failed to connect: ${errorMessage.split('\n')[0]}*`);
  102. }
  103. sections.push('');
  104. sections.push(
  105. `Use \`omos_skill_mcp\` tool with \`mcp_name="${serverName}"\` to invoke.`,
  106. );
  107. sections.push('');
  108. }
  109. return sections.join('\n');
  110. }
  111. export function createSkillTools(
  112. manager: SkillMcpManager,
  113. pluginConfig?: PluginConfig,
  114. ): {
  115. omos_skill: ToolDefinition;
  116. omos_skill_list: ToolDefinition;
  117. omos_skill_mcp: ToolDefinition;
  118. } {
  119. const allSkills = getBuiltinSkills();
  120. const description = SKILL_TOOL_DESCRIPTION;
  121. const skill: ToolDefinition = tool({
  122. description,
  123. args: {
  124. name: tool.schema
  125. .string()
  126. .describe('The skill identifier from available_skills'),
  127. },
  128. async execute(args: SkillArgs, toolContext) {
  129. const tctx = toolContext as ToolContext | undefined;
  130. const sessionId = tctx?.sessionID ? String(tctx.sessionID) : 'unknown';
  131. const agentName = tctx?.agent ?? 'orchestrator';
  132. const skillDefinition = getSkillByName(args.name);
  133. if (!skillDefinition) {
  134. const available = allSkills.map((s) => s.name).join(', ');
  135. throw new Error(
  136. `Skill "${args.name}" not found. Available skills: ${available || 'none'}`,
  137. );
  138. }
  139. // Check if this agent can use this skill
  140. if (!canAgentUseSkill(agentName, args.name, pluginConfig)) {
  141. const allowedSkills = getSkillsForAgent(agentName, pluginConfig);
  142. const allowedNames = allowedSkills.map((s) => s.name).join(', ');
  143. throw new Error(
  144. `Agent "${agentName}" cannot use skill "${args.name}". ` +
  145. `Available skills for this agent: ${allowedNames || 'none'}`,
  146. );
  147. }
  148. const output = [
  149. `## Skill: ${skillDefinition.name}`,
  150. '',
  151. skillDefinition.template.trim(),
  152. ];
  153. if (skillDefinition.mcpConfig) {
  154. const mcpInfo = await formatMcpCapabilities(
  155. skillDefinition,
  156. manager,
  157. sessionId,
  158. agentName,
  159. pluginConfig,
  160. );
  161. if (mcpInfo) {
  162. output.push(mcpInfo);
  163. }
  164. }
  165. return output.join('\n');
  166. },
  167. });
  168. const skill_list: ToolDefinition = tool({
  169. description: SKILL_LIST_TOOL_DESCRIPTION,
  170. args: {},
  171. async execute(_, toolContext) {
  172. const tctx = toolContext as ToolContext | undefined;
  173. const agentName = tctx?.agent ?? 'orchestrator';
  174. const skills = getSkillsForAgent(agentName, pluginConfig);
  175. return formatSkillsList(skills);
  176. },
  177. });
  178. const skill_mcp: ToolDefinition = tool({
  179. description: SKILL_MCP_TOOL_DESCRIPTION,
  180. args: {
  181. skillName: tool.schema
  182. .string()
  183. .describe('Skill name that provides the MCP'),
  184. mcpName: tool.schema.string().describe('MCP server name'),
  185. toolName: tool.schema.string().describe('Tool name to invoke'),
  186. toolArgs: tool.schema
  187. .record(tool.schema.string(), tool.schema.any())
  188. .optional(),
  189. },
  190. async execute(args: SkillMcpArgs, toolContext) {
  191. const tctx = toolContext as ToolContext | undefined;
  192. const sessionId = tctx?.sessionID ? String(tctx.sessionID) : 'unknown';
  193. const agentName = tctx?.agent ?? 'orchestrator';
  194. const skillDefinition = getSkillByName(args.skillName);
  195. if (!skillDefinition) {
  196. const available = allSkills.map((s) => s.name).join(', ');
  197. throw new Error(
  198. `Skill "${args.skillName}" not found. Available skills: ${available || 'none'}`,
  199. );
  200. }
  201. // Check if this agent can use this skill
  202. if (!canAgentUseSkill(agentName, args.skillName, pluginConfig)) {
  203. throw new Error(
  204. `Agent "${agentName}" cannot use skill "${args.skillName}".`,
  205. );
  206. }
  207. // Check if this agent can use this MCP
  208. if (!canAgentUseMcp(agentName, args.mcpName, pluginConfig)) {
  209. throw new Error(
  210. `Agent "${agentName}" cannot use MCP "${args.mcpName}".`,
  211. );
  212. }
  213. if (
  214. !skillDefinition.mcpConfig ||
  215. !skillDefinition.mcpConfig[args.mcpName]
  216. ) {
  217. throw new Error(
  218. `Skill "${args.skillName}" has no MCP named "${args.mcpName}".`,
  219. );
  220. }
  221. const config = skillDefinition.mcpConfig[args.mcpName];
  222. const info = {
  223. serverName: args.mcpName,
  224. skillName: skillDefinition.name,
  225. sessionId,
  226. };
  227. const result = await manager.callTool(
  228. info,
  229. config,
  230. args.toolName,
  231. args.toolArgs || {},
  232. );
  233. if (typeof result === 'string') {
  234. return result;
  235. }
  236. return JSON.stringify(result);
  237. },
  238. });
  239. return {
  240. omos_skill: skill,
  241. omos_skill_list: skill_list,
  242. omos_skill_mcp: skill_mcp,
  243. };
  244. }