add mcp server working
This commit is contained in:
parent
1d873aa82a
commit
1c614c9d28
76
index.html
76
index.html
|
|
@ -23,15 +23,20 @@
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
|
<div class="sidebar-section-header">
|
||||||
<div class="sidebar-section-title">SESSIONS</div>
|
<div class="sidebar-section-title">SESSIONS</div>
|
||||||
|
<button id="new-session" class="section-add-btn" title="New Session">+</button>
|
||||||
|
</div>
|
||||||
<div id="session-list"></div>
|
<div id="session-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-section">
|
||||||
<button id="new-session" class="btn-primary">
|
<div class="sidebar-section-header">
|
||||||
+ New Session
|
<div class="sidebar-section-title">MCP SERVERS</div>
|
||||||
</button>
|
<button id="add-mcp-server" class="section-add-btn" title="Add MCP Server">+</button>
|
||||||
|
</div>
|
||||||
|
<div id="mcp-server-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -80,6 +85,67 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- MCP Server Modal -->
|
||||||
|
<div id="mcp-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal">
|
||||||
|
<h2 class="modal-title">Add MCP Server</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Server Name</label>
|
||||||
|
<input type="text" id="mcp-name" class="form-input" placeholder="e.g., my-server" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Server Type</label>
|
||||||
|
<select id="mcp-type" class="form-select">
|
||||||
|
<option value="local">Local</option>
|
||||||
|
<option value="remote">Remote</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Local Server Fields -->
|
||||||
|
<div id="local-fields">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Command</label>
|
||||||
|
<input type="text" id="mcp-command" class="form-input" placeholder="e.g., node, npx" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Arguments (space-separated)</label>
|
||||||
|
<input type="text" id="mcp-args" class="form-input" placeholder="e.g., /path/to/server.js or -s @modelcontextprotocol/server-filesystem" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Environment Variables (optional, JSON)</label>
|
||||||
|
<textarea id="mcp-env" class="form-input" rows="3" placeholder='{"API_KEY": "your_api_key"}'></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remote Server Fields -->
|
||||||
|
<div id="remote-fields" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Server URL</label>
|
||||||
|
<input type="text" id="mcp-url" class="form-input" placeholder="https://your-server-url.com/mcp" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Headers (optional, JSON)</label>
|
||||||
|
<textarea id="mcp-headers" class="form-input" rows="3" placeholder='{"Authorization": "Bearer your-token"}'></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Always Allow Tools (optional, comma-separated)</label>
|
||||||
|
<input type="text" id="mcp-always-allow" class="form-input" placeholder="e.g., tool1, tool2, tool3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button id="cancel-mcp" class="btn-secondary">Cancel</button>
|
||||||
|
<button id="add-mcp" class="btn-primary">Add Server</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
require('./dist/renderer.js');
|
require('./dist/renderer.js');
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
80
main.ts
80
main.ts
|
|
@ -5,6 +5,10 @@ import * as path from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { simpleGit } from "simple-git";
|
import { simpleGit } from "simple-git";
|
||||||
import Store from "electron-store";
|
import Store from "electron-store";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
interface SessionConfig {
|
interface SessionConfig {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
|
|
@ -328,6 +332,82 @@ ipcMain.handle("get-all-sessions", () => {
|
||||||
return getPersistedSessions();
|
return getPersistedSessions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MCP Server management functions
|
||||||
|
async function listMcpServers() {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync("claude mcp list");
|
||||||
|
|
||||||
|
if (stdout.includes("No MCP servers configured")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = stdout.trim().split("\n").filter(line => line.trim());
|
||||||
|
const servers = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Skip header lines, empty lines, and status messages
|
||||||
|
if (line.includes("MCP servers") ||
|
||||||
|
line.includes("---") ||
|
||||||
|
line.includes("Checking") ||
|
||||||
|
line.includes("health") ||
|
||||||
|
!line.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse format: "name: url (type) - status" or just "name"
|
||||||
|
// Extract just the server name (before the colon)
|
||||||
|
const colonIndex = line.indexOf(":");
|
||||||
|
const serverName = colonIndex > 0 ? line.substring(0, colonIndex).trim() : line.trim();
|
||||||
|
|
||||||
|
if (serverName) {
|
||||||
|
servers.push({ name: serverName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error listing MCP servers:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMcpServer(name: string, config: any) {
|
||||||
|
// Use add-json to support full configuration including env vars, headers, etc.
|
||||||
|
const jsonConfig = JSON.stringify(config);
|
||||||
|
await execAsync(`claude mcp add-json "${name}" '${jsonConfig}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMcpServer(name: string) {
|
||||||
|
await execAsync(`claude mcp remove "${name}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("list-mcp-servers", async () => {
|
||||||
|
try {
|
||||||
|
return await listMcpServers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error listing MCP servers:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("add-mcp-server", async (_event, name: string, config: any) => {
|
||||||
|
try {
|
||||||
|
await addMcpServer(name, config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding MCP server:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("remove-mcp-server", async (_event, name: string) => {
|
||||||
|
try {
|
||||||
|
await removeMcpServer(name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error removing MCP server:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const createWindow = () => {
|
const createWindow = () => {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1400,
|
width: 1400,
|
||||||
|
|
|
||||||
183
renderer.ts
183
renderer.ts
|
|
@ -28,8 +28,18 @@ interface Session {
|
||||||
hasActivePty: boolean;
|
hasActivePty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface McpServer {
|
||||||
|
name: string;
|
||||||
|
command?: string;
|
||||||
|
args?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
url?: string;
|
||||||
|
type?: "stdio" | "sse";
|
||||||
|
}
|
||||||
|
|
||||||
const sessions = new Map<string, Session>();
|
const sessions = new Map<string, Session>();
|
||||||
let activeSessionId: string | null = null;
|
let activeSessionId: string | null = null;
|
||||||
|
let mcpServers: McpServer[] = [];
|
||||||
|
|
||||||
function createTerminalUI(sessionId: string) {
|
function createTerminalUI(sessionId: string) {
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
|
|
@ -461,3 +471,176 @@ createBtn?.addEventListener("click", () => {
|
||||||
parentBranchSelect.innerHTML = '<option value="">Loading branches...</option>';
|
parentBranchSelect.innerHTML = '<option value="">Loading branches...</option>';
|
||||||
codingAgentSelect.value = "claude";
|
codingAgentSelect.value = "claude";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MCP Server management functions
|
||||||
|
async function loadMcpServers() {
|
||||||
|
try {
|
||||||
|
const servers = await ipcRenderer.invoke("list-mcp-servers");
|
||||||
|
mcpServers = servers;
|
||||||
|
renderMcpServers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load MCP servers:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMcpServers() {
|
||||||
|
const list = document.getElementById("mcp-server-list");
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
mcpServers.forEach(server => {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.className = "session-list-item";
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-2 flex-1">
|
||||||
|
<span class="session-indicator active"></span>
|
||||||
|
<span class="truncate">${server.name}</span>
|
||||||
|
</div>
|
||||||
|
<button class="session-delete-btn mcp-remove-btn" data-name="${server.name}" title="Remove server">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const removeBtn = item.querySelector(".mcp-remove-btn");
|
||||||
|
removeBtn?.addEventListener("click", async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm(`Remove MCP server "${server.name}"?`)) {
|
||||||
|
try {
|
||||||
|
await ipcRenderer.invoke("remove-mcp-server", server.name);
|
||||||
|
await loadMcpServers();
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Failed to remove server: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP Modal handling
|
||||||
|
const mcpModal = document.getElementById("mcp-modal");
|
||||||
|
const mcpNameInput = document.getElementById("mcp-name") as HTMLInputElement;
|
||||||
|
const mcpTypeSelect = document.getElementById("mcp-type") as HTMLSelectElement;
|
||||||
|
const mcpCommandInput = document.getElementById("mcp-command") as HTMLInputElement;
|
||||||
|
const mcpArgsInput = document.getElementById("mcp-args") as HTMLInputElement;
|
||||||
|
const mcpEnvInput = document.getElementById("mcp-env") as HTMLTextAreaElement;
|
||||||
|
const mcpUrlInput = document.getElementById("mcp-url") as HTMLInputElement;
|
||||||
|
const mcpHeadersInput = document.getElementById("mcp-headers") as HTMLTextAreaElement;
|
||||||
|
const mcpAlwaysAllowInput = document.getElementById("mcp-always-allow") as HTMLInputElement;
|
||||||
|
const localFields = document.getElementById("local-fields");
|
||||||
|
const remoteFields = document.getElementById("remote-fields");
|
||||||
|
const cancelMcpBtn = document.getElementById("cancel-mcp");
|
||||||
|
const addMcpBtn = document.getElementById("add-mcp");
|
||||||
|
|
||||||
|
// Toggle fields based on server type
|
||||||
|
mcpTypeSelect?.addEventListener("change", () => {
|
||||||
|
if (mcpTypeSelect.value === "local") {
|
||||||
|
localFields!.style.display = "block";
|
||||||
|
remoteFields!.style.display = "none";
|
||||||
|
} else {
|
||||||
|
localFields!.style.display = "none";
|
||||||
|
remoteFields!.style.display = "block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add MCP server button - opens modal
|
||||||
|
document.getElementById("add-mcp-server")?.addEventListener("click", () => {
|
||||||
|
mcpModal?.classList.remove("hidden");
|
||||||
|
mcpNameInput.value = "";
|
||||||
|
mcpTypeSelect.value = "local";
|
||||||
|
mcpCommandInput.value = "";
|
||||||
|
mcpArgsInput.value = "";
|
||||||
|
mcpEnvInput.value = "";
|
||||||
|
mcpUrlInput.value = "";
|
||||||
|
mcpHeadersInput.value = "";
|
||||||
|
mcpAlwaysAllowInput.value = "";
|
||||||
|
localFields!.style.display = "block";
|
||||||
|
remoteFields!.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel MCP button
|
||||||
|
cancelMcpBtn?.addEventListener("click", () => {
|
||||||
|
mcpModal?.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add MCP button
|
||||||
|
addMcpBtn?.addEventListener("click", async () => {
|
||||||
|
const name = mcpNameInput.value.trim();
|
||||||
|
const serverType = mcpTypeSelect.value;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
alert("Please enter a server name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: any = {};
|
||||||
|
|
||||||
|
if (serverType === "local") {
|
||||||
|
config.type = "stdio";
|
||||||
|
|
||||||
|
const command = mcpCommandInput.value.trim();
|
||||||
|
const argsInput = mcpArgsInput.value.trim();
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
alert("Please enter a command");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.command = command;
|
||||||
|
if (argsInput) {
|
||||||
|
config.args = argsInput.split(" ").filter(a => a.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse environment variables if provided
|
||||||
|
const envInput = mcpEnvInput.value.trim();
|
||||||
|
if (envInput) {
|
||||||
|
try {
|
||||||
|
config.env = JSON.parse(envInput);
|
||||||
|
} catch (error) {
|
||||||
|
alert("Invalid JSON for environment variables");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remote server
|
||||||
|
config.type = "sse";
|
||||||
|
|
||||||
|
const url = mcpUrlInput.value.trim();
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
alert("Please enter a server URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.url = url;
|
||||||
|
|
||||||
|
// Parse headers if provided
|
||||||
|
const headersInput = mcpHeadersInput.value.trim();
|
||||||
|
if (headersInput) {
|
||||||
|
try {
|
||||||
|
config.headers = JSON.parse(headersInput);
|
||||||
|
} catch (error) {
|
||||||
|
alert("Invalid JSON for headers");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse always allow tools
|
||||||
|
const alwaysAllowInput = mcpAlwaysAllowInput.value.trim();
|
||||||
|
if (alwaysAllowInput) {
|
||||||
|
config.alwaysAllow = alwaysAllowInput.split(",").map(t => t.trim()).filter(t => t);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ipcRenderer.invoke("add-mcp-server", name, config);
|
||||||
|
await loadMcpServers();
|
||||||
|
mcpModal?.classList.add("hidden");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding server:", error);
|
||||||
|
alert(`Failed to add server: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load MCP servers on startup
|
||||||
|
loadMcpServers();
|
||||||
|
|
|
||||||
98
simple-claude.ts
Normal file
98
simple-claude.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
export interface McpServer {
|
||||||
|
name: string;
|
||||||
|
command?: string;
|
||||||
|
args?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
url?: string;
|
||||||
|
type?: "stdio" | "sse";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SimpleClaude {
|
||||||
|
private claudePath: string;
|
||||||
|
|
||||||
|
constructor(claudePath: string = "claude") {
|
||||||
|
this.claudePath = claudePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listMcpServers(): Promise<McpServer[]> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`${this.claudePath} mcp list`);
|
||||||
|
|
||||||
|
// If no servers configured, return empty array
|
||||||
|
if (stdout.includes("No MCP servers configured")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the output - format is typically a list of server names
|
||||||
|
// We'll need to get details for each server
|
||||||
|
const lines = stdout.trim().split("\n").filter(line => line.trim());
|
||||||
|
const servers: McpServer[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Skip header lines
|
||||||
|
if (line.includes("MCP servers") || line.includes("---")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverName = line.trim();
|
||||||
|
if (serverName) {
|
||||||
|
try {
|
||||||
|
const details = await this.getMcpServer(serverName);
|
||||||
|
servers.push(details);
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't get details, just add the name
|
||||||
|
servers.push({ name: serverName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error listing MCP servers:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMcpServer(name: string): Promise<McpServer> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`${this.claudePath} mcp get "${name}"`);
|
||||||
|
|
||||||
|
// Try to parse as JSON if possible
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stdout);
|
||||||
|
return { name, ...parsed };
|
||||||
|
} catch {
|
||||||
|
// If not JSON, return basic info
|
||||||
|
return { name };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to get MCP server ${name}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMcpServer(name: string, command: string, args: string[] = []): Promise<void> {
|
||||||
|
try {
|
||||||
|
const argsStr = args.join(" ");
|
||||||
|
await execAsync(`${this.claudePath} mcp add "${name}" ${command} ${argsStr}`);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to add MCP server ${name}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeMcpServer(name: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await execAsync(`${this.claudePath} mcp remove "${name}"`);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to remove MCP server ${name}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function simpleClaude(claudePath?: string): SimpleClaude {
|
||||||
|
return new SimpleClaude(claudePath);
|
||||||
|
}
|
||||||
14
styles.css
14
styles.css
|
|
@ -24,12 +24,16 @@
|
||||||
@apply p-2;
|
@apply p-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section-title {
|
.sidebar-section-header {
|
||||||
@apply text-xs text-gray-400 px-2 py-1 mb-1;
|
@apply flex items-center justify-between px-2 py-1 mb-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-section-title {
|
||||||
@apply p-3 border-t border-gray-700;
|
@apply text-xs text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-add-btn {
|
||||||
|
@apply text-gray-400 hover:text-white hover:bg-gray-700 text-sm w-5 h-5 flex items-center justify-center rounded transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session List Item */
|
/* Session List Item */
|
||||||
|
|
@ -122,6 +126,8 @@
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
@apply w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500;
|
@apply w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-select {
|
.form-select {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue