Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ See [references/SKILLS.md](references/SKILLS.md) for details.
| Action | Command |
|--------|---------|
| **Authenticate** | `smithery login` |
| **Log out** | `smithery logout` |
| **Check auth** | `smithery whoami` |
| **Search MCP servers** | `smithery search [term]` |
| **Search skills** | `smithery skills search [term]` |
Expand Down
13 changes: 13 additions & 0 deletions references/AUTH.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ This will:

The CLI will wait up to 5 minutes for confirmation.

## Logout

To remove all local credentials:

```bash
smithery logout
```

This removes:
- API key from local settings
- Namespace configuration
- All server configurations from keychain

## Check Auth Status

```bash
Expand Down
33 changes: 28 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,31 @@ program
}
})

// Logout command
program
.command("logout")
.description("Log out and remove all local credentials")
.action(async () => {
const { clearApiKey, clearNamespace } = await import(
"./utils/smithery-settings"
)
const { clearAllConfigs } = await import("./lib/keychain.js")

console.log(chalk.cyan("Logging out of Smithery..."))

// Clear API key
await clearApiKey()

// Clear namespace
await clearNamespace()

// Clear keychain entries
await clearAllConfigs()

console.log(chalk.green("✓ Successfully logged out"))
console.log(chalk.gray("All local credentials have been removed"))
})

// Show API key command
program
.command("whoami")
Expand Down Expand Up @@ -539,11 +564,9 @@ program
console.log(chalk.cyan("API Key:"), masked)
console.log(chalk.gray("Use --full to display the complete key"))
}
} catch (error) {
console.error(chalk.red("✗ Failed to retrieve API key"))
const errorMessage =
error instanceof Error ? error.message : String(error)
console.error(chalk.gray(errorMessage))
} catch (_error) {
console.log(chalk.yellow("Not logged in"))
console.log(chalk.gray("Run 'smithery login' to authenticate"))
process.exit(1)
}
})
Expand Down
32 changes: 32 additions & 0 deletions src/lib/keychain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,35 @@ export async function deleteConfig(qualifiedName: string): Promise<void> {
)
}
}

/**
* Clear all server configurations from OS keychain
*/
export async function clearAllConfigs(): Promise<void> {
const kt = getKeytar()
if (!kt) {
verbose("Keychain not available, skipping clear all configs")
return
}

verbose("Clearing all configs from keychain")
try {
// keytar v7.9+ has findCredentials
const credentials = await (
kt as Keytar & {
findCredentials(
service: string,
): Promise<Array<{ account: string; password: string }>>
}
).findCredentials(SERVICE_NAME)
for (const cred of credentials) {
await kt.deletePassword(SERVICE_NAME, cred.account)
verbose(`Deleted keychain entry: ${cred.account}`)
}
verbose(`Cleared ${credentials.length} keychain entries`)
} catch (error) {
verbose(
`Failed to clear keychain configs: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
13 changes: 13 additions & 0 deletions src/utils/smithery-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,19 @@ export const clearApiKey = async (): Promise<SettingsResult> => {
return await saveSettings(settingsData, getSettingsPath())
}

export const clearNamespace = async (): Promise<SettingsResult> => {
const initResult = await initializeSettings()
if (!initResult.success || !initResult.data) {
return initResult
}

// Remove namespace from settings
const { namespace: _removed, ...settingsWithoutNamespace } = initResult.data
settingsData = settingsWithoutNamespace as Settings

return await saveSettings(settingsData, getSettingsPath())
}

export const hasAskedConsent = async (): Promise<boolean> => {
await initializeSettings()
return settingsData?.askedConsent || false
Expand Down