Skip to content

Per agent MCPs - #78

Merged
alvinunreal merged 7 commits into
masterfrom
permission
Jan 23, 2026
Merged

Per agent MCPs#78
alvinunreal merged 7 commits into
masterfrom
permission

Conversation

@alvinunreal

Copy link
Copy Markdown
Owner

Summary

Changes

@alvinunreal
alvinunreal force-pushed the permission branch 2 times, most recently from 1301e2d to a7451f7 Compare January 23, 2026 20:28
@greptile-apps

greptile-apps Bot commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements per-agent MCP (Model Context Protocol) permissions, allowing fine-grained control over which agents can access which MCP servers. The implementation mirrors the existing skill permission system with wildcard (*) and exclusion (!item) syntax support.

Key Changes:

  • Added mcps field to agent configuration schema supporting wildcard and exclusion syntax
  • Implemented DEFAULT_AGENT_MCPS mapping (orchestrator gets websearch, librarian gets websearch/context7/grep_app)
  • Created parseList() utility function for wildcard/exclusion resolution shared by skills and MCPs
  • Added MCP permission compilation that translates agent MCP lists into OpenCode permission rules
  • Implemented runtime permission checks in skill MCP invocation (canAgentUseMcp)
  • Renamed yagni-enforcement skill to simplify for clarity
  • Added comprehensive test coverage for new MCP permission functionality
  • Updated documentation with MCP permission examples

Implementation Quality:
The changes follow the existing skill permission pattern closely, maintaining consistency across the codebase. Permission compilation happens during plugin initialization and uses a "deny wins" principle for conflicts. The implementation includes proper backward compatibility handling through agent aliases.

Confidence Score: 4/5

  • Safe to merge with minor cleanup recommended for unused variables
  • The PR implements a well-tested feature following existing patterns. The core logic is sound with comprehensive test coverage. Two minor style issues exist (unused imports/variables) that don't affect functionality but should be cleaned up. The permission system correctly implements "deny wins" principle and handles edge cases properly.
  • src/index.ts has unused variable permissionConfig and unused import canAgentUseMcp that should be removed

Important Files Changed

Filename Overview
src/config/schema.ts Added mcps field to agent configuration schema for per-agent MCP permissions
src/tools/skill/builtin.ts Added DEFAULT_AGENT_MCPS, parseList, canAgentUseMcp, and getAgentMcpList functions; renamed skill from yagni-enforcement to simplify
src/agents/index.ts Added mcps field to agent configs via getAgentMcpList call
src/index.ts Added MCP permission compilation logic that translates per-agent MCP lists into OpenCode permission rules; unused permissionConfig variable declared
src/tools/skill/tools.ts Added canAgentUseMcp permission check in skill MCP invocation flow

Sequence Diagram

sequenceDiagram
    participant User
    participant Plugin as Plugin (index.ts)
    participant AgentFactory as Agent Factory (agents/index.ts)
    participant BuiltinSkills as Builtin Skills (tools/skill/builtin.ts)
    participant SkillTools as Skill Tools (tools/skill/tools.ts)
    participant OpenCodeSDK as OpenCode SDK

    Note over User,OpenCodeSDK: Plugin Initialization

    User->>Plugin: Initialize plugin with config
    Plugin->>AgentFactory: getAgentConfigs(config)
    AgentFactory->>BuiltinSkills: getAgentMcpList(agentName, config)
    BuiltinSkills->>BuiltinSkills: Check config override or use DEFAULT_AGENT_MCPS
    BuiltinSkills-->>AgentFactory: Return mcps list (e.g., ["websearch", "context7"])
    AgentFactory-->>Plugin: Return agents with mcps field
    
    Plugin->>Plugin: Compile MCP permissions
    loop For each agent
        Plugin->>BuiltinSkills: parseList(agentMcps, allMcpNames)
        BuiltinSkills-->>Plugin: Return allowed MCPs
        loop For each MCP
            Plugin->>Plugin: Set permission rule (e.g., "websearch_*": "allow")
        end
    end
    
    Plugin->>OpenCodeSDK: Register agents with permissions
    
    Note over User,OpenCodeSDK: Skill Invocation with MCP

    User->>SkillTools: Invoke omos_skill_mcp(skillName, mcpName, toolName)
    SkillTools->>BuiltinSkills: canAgentUseMcp(agentName, mcpName, config)
    BuiltinSkills->>BuiltinSkills: getAgentMcpList(agentName, config)
    BuiltinSkills->>BuiltinSkills: parseList(mcpList, availableMcps)
    BuiltinSkills-->>SkillTools: Return true/false
    
    alt Agent has permission
        SkillTools->>SkillTools: Call MCP tool
        SkillTools-->>User: Return result
    else Agent lacks permission
        SkillTools-->>User: Throw error
    end
Loading

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/index.ts Outdated
} from './tools';
import { startTmuxCheck } from './utils';
import { log } from './utils/logger';
import { canAgentUseMcp, parseList } from './tools/skill/builtin';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: importing canAgentUseMcp but it's not used in this file

Suggested change
import { canAgentUseMcp, parseList } from './tools/skill/builtin';
import { parseList } from './tools/skill/builtin';

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.ts
Line: 26:26

Comment:
**style:** importing `canAgentUseMcp` but it's not used in this file

```suggestion
import { parseList } from './tools/skill/builtin';
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/index.ts Outdated
Comment on lines +120 to +122
const permissionConfig = opencodeConfig.permission as
| Record<string, unknown>
| undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: permissionConfig is declared but never used in the permission compilation logic

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.ts
Line: 120:122

Comment:
**style:** `permissionConfig` is declared but never used in the permission compilation logic

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@alvinunreal

Copy link
Copy Markdown
Owner Author

@greptile

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +438 to +501
describe('parseList', () => {
test('returns empty array for empty input', () => {
expect(parseList([], ['a', 'b', 'c'])).toEqual([]);
});

test('returns empty array for undefined input', () => {
expect(parseList(undefined as any, ['a', 'b', 'c'])).toEqual([]);
});

test('returns explicit items when no wildcard', () => {
expect(parseList(['a', 'c'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
});

test('expands wildcard to all available items', () => {
expect(parseList(['*'], ['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
});

test('excludes items with ! prefix', () => {
expect(parseList(['*', '!b'], ['a', 'b', 'c'])).toEqual(['a', 'c']);
});

test('excludes multiple items with ! prefix', () => {
expect(parseList(['*', '!b', '!c'], ['a', 'b', 'c', 'd'])).toEqual([
'a',
'd',
]);
});

test('deny wins in case of conflict', () => {
expect(parseList(['a', '!a'], ['a', 'b'])).toEqual([]);
});

test('!* denies all items', () => {
expect(parseList(['!*'], ['a', 'b', 'c'])).toEqual([]);
});
});

describe('DEFAULT_AGENT_MCPS', () => {
test('orchestrator has websearch MCP', () => {
expect(DEFAULT_AGENT_MCPS.orchestrator).toContain('websearch');
});

test('designer has no MCPs by default', () => {
expect(DEFAULT_AGENT_MCPS.designer).toEqual([]);
});

test('oracle has no MCPs by default', () => {
expect(DEFAULT_AGENT_MCPS.oracle).toEqual([]);
});

test('librarian has websearch, context7, and grep_app MCPs', () => {
expect(DEFAULT_AGENT_MCPS.librarian).toContain('websearch');
expect(DEFAULT_AGENT_MCPS.librarian).toContain('context7');
expect(DEFAULT_AGENT_MCPS.librarian).toContain('grep_app');
});

test('explorer has no MCPs by default', () => {
expect(DEFAULT_AGENT_MCPS.explorer).toEqual([]);
});

test('fixer has no MCPs by default', () => {
expect(DEFAULT_AGENT_MCPS.fixer).toEqual([]);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Duplicate test suites - parseList and DEFAULT_AGENT_MCPS are tested twice with identical test cases

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tools/skill/builtin.test.ts
Line: 438:501

Comment:
**logic:** Duplicate test suites - parseList and DEFAULT_AGENT_MCPS are tested twice with identical test cases

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +503 to +642
describe('getAgentMcpList', () => {
test('returns default MCPs for orchestrator', () => {
const mcps = getAgentMcpList('orchestrator');
expect(mcps).toEqual(DEFAULT_AGENT_MCPS.orchestrator);
});

test('returns default MCPs for librarian', () => {
const mcps = getAgentMcpList('librarian');
expect(mcps).toEqual(DEFAULT_AGENT_MCPS.librarian);
});

test('returns empty for designer', () => {
const mcps = getAgentMcpList('designer');
expect(mcps).toEqual([]);
});

test('respects config override for agent MCPs', () => {
const config: PluginConfig = {
agents: {
oracle: { mcps: ['websearch'] },
},
};
const mcps = getAgentMcpList('oracle', config);
expect(mcps).toEqual(['websearch']);
});

test('config wildcard overrides default', () => {
const config: PluginConfig = {
agents: {
designer: { mcps: ['*'] },
},
};
const mcps = getAgentMcpList('designer', config);
expect(mcps).toEqual(['*']);
});

test('config empty array removes default MCPs', () => {
const config: PluginConfig = {
agents: {
librarian: { mcps: [] },
},
};
const mcps = getAgentMcpList('librarian', config);
expect(mcps).toEqual([]);
});

test('backward compat: alias config applies to agent', () => {
const config: PluginConfig = {
agents: {
explore: { mcps: ['websearch'] },
},
};
const mcps = getAgentMcpList('explorer', config);
expect(mcps).toEqual(['websearch']);
});

test('returns empty for unknown agent without config', () => {
const mcps = getAgentMcpList('unknown-agent');
expect(mcps).toEqual([]);
});
});

describe('canAgentUseMcp', () => {
test('orchestrator can use websearch by default', () => {
expect(canAgentUseMcp('orchestrator', 'websearch')).toBe(true);
});

test('librarian can use websearch, context7, and grep_app by default', () => {
expect(canAgentUseMcp('librarian', 'websearch')).toBe(true);
expect(canAgentUseMcp('librarian', 'context7')).toBe(true);
expect(canAgentUseMcp('librarian', 'grep_app')).toBe(true);
});

test('designer cannot use any MCP by default', () => {
expect(canAgentUseMcp('designer', 'websearch')).toBe(false);
expect(canAgentUseMcp('designer', 'context7')).toBe(false);
});

test('respects config override', () => {
const config: PluginConfig = {
agents: {
oracle: { mcps: ['websearch'] },
},
};
expect(canAgentUseMcp('oracle', 'websearch', config)).toBe(true);
expect(canAgentUseMcp('oracle', 'context7', config)).toBe(false);
});

test('config wildcard grants all MCP permissions', () => {
const config: PluginConfig = {
agents: {
designer: { mcps: ['*'] },
},
};
expect(canAgentUseMcp('designer', 'websearch', config)).toBe(true);
});

test('config wildcard grants skill MCP permissions', () => {
const config: PluginConfig = {
agents: {
designer: { mcps: ['*'] },
},
};
expect(canAgentUseMcp('designer', 'playwright', config)).toBe(true);
});

test('config empty array denies all MCPs', () => {
const config: PluginConfig = {
agents: {
librarian: { mcps: [] },
},
};
expect(canAgentUseMcp('librarian', 'websearch', config)).toBe(false);
});

test('respects exclusion syntax', () => {
const config: PluginConfig = {
agents: {
orchestrator: { mcps: ['*', '!websearch'] },
},
};
// canAgentUseMcp uses DEFAULT_AGENT_MCPS.orchestrator keys as allAvailable
// which is ['websearch'], so excluding websearch leaves empty
expect(canAgentUseMcp('orchestrator', 'websearch', config)).toBe(false);
});

test('backward compat: alias config affects agent permissions', () => {
const config: PluginConfig = {
agents: {
explore: { mcps: ['websearch'] },
},
};
expect(canAgentUseMcp('explorer', 'websearch', config)).toBe(true);
expect(canAgentUseMcp('explorer', 'context7', config)).toBe(false);
});

test('unknown agent returns false without config', () => {
expect(canAgentUseMcp('unknown-agent', 'websearch')).toBe(false);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Duplicate test suites - getAgentMcpList and canAgentUseMcp test suites appear to be duplicated

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tools/skill/builtin.test.ts
Line: 503:642

Comment:
**logic:** Duplicate test suites - getAgentMcpList and canAgentUseMcp test suites appear to be duplicated

How can I resolve this? If you propose a fix, please make it concise.

@alvinunreal

Copy link
Copy Markdown
Owner Author

@greptile

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/index.ts Outdated
Comment on lines +120 to +122
const permissionConfig = opencodeConfig.permission as
| Record<string, unknown>
| undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: permissionConfig is declared but never used

Suggested change
const permissionConfig = opencodeConfig.permission as
| Record<string, unknown>
| undefined;
// Compile MCP permissions into PermissionNext rules
// This maps our simple mcps config to opencode's permission system
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.ts
Line: 120:122

Comment:
**style:** `permissionConfig` is declared but never used

```suggestion
      // Compile MCP permissions into PermissionNext rules
      // This maps our simple mcps config to opencode's permission system
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/index.ts Outdated
} from './tools';
import { startTmuxCheck } from './utils';
import { log } from './utils/logger';
import { canAgentUseMcp, parseList } from './tools/skill/builtin';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: canAgentUseMcp is imported but never used in this file

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.ts
Line: 26:26

Comment:
**style:** `canAgentUseMcp` is imported but never used in this file

How can I resolve this? If you propose a fix, please make it concise.

@alvinunreal
alvinunreal force-pushed the permission branch 2 times, most recently from 3bf888c to 7eae8c6 Compare January 23, 2026 23:21
@alvinunreal
alvinunreal merged commit 4102b50 into master Jan 23, 2026
1 check passed
nghyane pushed a commit to nghyane/oh-my-opencode-slim that referenced this pull request Jan 31, 2026
@mhenke
mhenke deleted the permission branch July 10, 2026 16:14
mhenke pushed a commit to mhenke/oh-my-opencode-slim that referenced this pull request Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant