intercept-mcp 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ # Custom
2
+ .idea/
3
+ .claude/settings.local.json
4
+ .playwright-mcp/
5
+ .superpowers/
6
+
7
+ # Log directory (keep .gitkeep, ignore everything else)
8
+ logs/*
9
+ !logs/.gitkeep
10
+
11
+ # Redis dump files
12
+ *.rdb
13
+
14
+ # UV lock file (each service manages its own dependencies)
15
+ uv.lock
16
+
17
+ # SQLite databases (catch-all — no .db files should be tracked)
18
+ *.db
19
+ *.db-journal
20
+ *.db-wal
21
+ *.db-shm
22
+
23
+ # Local data directories (cloned repos, reports, etc.)
24
+ **/data/clones/
25
+ **/data/reports/
26
+
27
+ # Playwright
28
+ playwright-report/
29
+ test-results/
30
+ **/e2e/screenshots/*.png
31
+
32
+ # Node.js / Next.js
33
+ node_modules/
34
+ .pnp
35
+ .pnp.*
36
+ .yarn/*
37
+ !.yarn/patches
38
+ !.yarn/plugins
39
+ !.yarn/releases
40
+ !.yarn/versions
41
+ .next/
42
+ out/
43
+ *.tsbuildinfo
44
+ next-env.d.ts
45
+ .vercel
46
+ npm-debug.log*
47
+ yarn-debug.log*
48
+ yarn-error.log*
49
+ .pnpm-debug.log*
50
+
51
+ # Byte-compiled / optimized / DLL files
52
+ __pycache__/
53
+ *.py[codz]
54
+ *$py.class
55
+
56
+ # C extensions
57
+ *.so
58
+
59
+ # Distribution / packaging
60
+ .Python
61
+ build/
62
+ develop-eggs/
63
+ dist/
64
+ downloads/
65
+ eggs/
66
+ .eggs/
67
+ /lib/
68
+ /lib64/
69
+ parts/
70
+ sdist/
71
+ var/
72
+ wheels/
73
+ share/python-wheels/
74
+ *.egg-info/
75
+ .installed.cfg
76
+ *.egg
77
+ MANIFEST
78
+
79
+ # PyInstaller
80
+ # Usually these files are written by a python script from a template
81
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
82
+ *.manifest
83
+ *.spec
84
+
85
+ # Installer logs
86
+ pip-log.txt
87
+ pip-delete-this-directory.txt
88
+
89
+ # Unit test / coverage reports
90
+ htmlcov/
91
+ coverage/
92
+ .tox/
93
+ .nox/
94
+ .coverage
95
+ .coverage.*
96
+ .cache
97
+ nosetests.xml
98
+ coverage.xml
99
+ *.cover
100
+ *.py.cover
101
+ .hypothesis/
102
+ .pytest_cache/
103
+ cover/
104
+
105
+ # Translations
106
+ *.mo
107
+ *.pot
108
+
109
+ # Django stuff:
110
+ *.log
111
+ local_settings.py
112
+ db.sqlite3
113
+ db.sqlite3-journal
114
+
115
+ # Flask stuff:
116
+ instance/
117
+ .webassets-cache
118
+
119
+ # Scrapy stuff:
120
+ .scrapy
121
+
122
+ # Sphinx documentation
123
+ docs/_build/
124
+
125
+ # PyBuilder
126
+ .pybuilder/
127
+ target/
128
+
129
+ # Jupyter Notebook
130
+ .ipynb_checkpoints
131
+
132
+ # IPython
133
+ profile_default/
134
+ ipython_config.py
135
+
136
+ # pyenv
137
+ # For a library or package, you might want to ignore these files since the code is
138
+ # intended to run in multiple environments; otherwise, check them in:
139
+ # .python-version
140
+
141
+ # pipenv
142
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
143
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
144
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
145
+ # install all needed dependencies.
146
+ #Pipfile.lock
147
+
148
+ # UV
149
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
150
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
151
+ # commonly ignored for libraries.
152
+ #uv.lock
153
+
154
+ # poetry
155
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
156
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
157
+ # commonly ignored for libraries.
158
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
159
+ #poetry.lock
160
+ #poetry.toml
161
+
162
+ # pdm
163
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
164
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
165
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
166
+ #pdm.lock
167
+ #pdm.toml
168
+ .pdm-python
169
+ .pdm-build/
170
+
171
+ # pixi
172
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
173
+ #pixi.lock
174
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
175
+ # in the .venv directory. It is recommended not to include this directory in version control.
176
+ .pixi
177
+
178
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
179
+ __pypackages__/
180
+
181
+ # Celery stuff
182
+ celerybeat-schedule
183
+ celerybeat.pid
184
+
185
+ # SageMath parsed files
186
+ *.sage.py
187
+
188
+ # Environments
189
+ .env
190
+ .envrc
191
+ .venv
192
+ env/
193
+ venv/
194
+ ENV/
195
+ env.bak/
196
+ venv.bak/
197
+
198
+ # Spyder project settings
199
+ .spyderproject
200
+ .spyproject
201
+
202
+ # Rope project settings
203
+ .ropeproject
204
+
205
+ # mkdocs documentation
206
+ /site
207
+
208
+ # mypy
209
+ .mypy_cache/
210
+ .dmypy.json
211
+ dmypy.json
212
+
213
+ # Pyre type checker
214
+ .pyre/
215
+
216
+ # pytype static type analyzer
217
+ .pytype/
218
+
219
+ # Cython debug symbols
220
+ cython_debug/
221
+
222
+ # PyCharm
223
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
224
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
225
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
226
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
227
+ #.idea/
228
+
229
+ # Abstra
230
+ # Abstra is an AI-powered process automation framework.
231
+ # Ignore directories containing user credentials, local state, and settings.
232
+ # Learn more at https://abstra.io/docs
233
+ .abstra/
234
+
235
+ # Visual Studio Code
236
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
237
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
238
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
239
+ # you could uncomment the following to ignore the entire vscode folder
240
+ # .vscode/
241
+
242
+ # Ruff stuff:
243
+ .ruff_cache/
244
+
245
+ # PyPI configuration file
246
+ .pypirc
247
+
248
+ # Cursor
249
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
250
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
251
+ # refer to https://docs.cursor.com/context/ignore-files
252
+ .cursorignore
253
+ .cursorindexingignore
254
+
255
+ # Marimo
256
+ marimo/_static/
257
+ marimo/_lsp/
258
+ __marimo__/
259
+ .DS_Store
260
+ **/.DS_Store
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: intercept-mcp
3
+ Version: 0.2.2
4
+ Summary: MCP server exposing Intercept supply chain security data to MCP-compatible clients.
5
+ Project-URL: Homepage, https://intercept.hijacksecurity.com
6
+ Project-URL: Documentation, https://intercept.hijacksecurity.com/docs
7
+ Project-URL: Repository, https://github.com/hijacksecurity/Intercept
8
+ Author-email: Hijack Security <support@hijacksecurity.com>
9
+ License: Proprietary
10
+ Keywords: intercept,mcp,model-context-protocol,sast,security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: ftfy>=6.2.0
22
+ Requires-Dist: httpx>=0.28.0
23
+ Requires-Dist: mcp>=1.27.0
24
+ Requires-Dist: pydantic>=2.10.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
28
+ Requires-Dist: pytest-xdist>=3.5.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # intercept-mcp
33
+
34
+ Model Context Protocol (MCP) server that exposes an [Intercept](https://intercept.hijacksecurity.com) tenant's repositories, findings, scans, and resolutions to MCP-compatible AI clients.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ uvx intercept-mcp
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ | Variable | Required | Default |
45
+ |---|---|---|
46
+ | `INTERCEPT_MCP_API_KEY` | yes | — |
47
+ | `INTERCEPT_API_URL` | no | `https://intercept.hijacksecurity.com` |
48
+
49
+ `INTERCEPT_MCP_API_KEY` is a personal-scope API key, generated from the Intercept web UI: **Settings → Integrations → Generate MCP API Key**. The value starts with `hsk_`.
50
+
51
+ `INTERCEPT_API_URL` defaults to Intercept production. Override with the URL provided by your Intercept administrator for other environments.
52
+
53
+ ## Claude Code configuration
54
+
55
+ Export the key from your shell profile (`~/.zshrc`, `~/.bashrc`):
56
+
57
+ ```bash
58
+ export INTERCEPT_MCP_API_KEY=hsk_xxxxxxxx
59
+ ```
60
+
61
+ Then add the server to your MCP client config:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "intercept": {
67
+ "command": "uvx",
68
+ "args": ["intercept-mcp"]
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ Restart the client. Verify with `claude mcp list`.
75
+
76
+ If you prefer to keep the key in the client config instead of the shell:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "intercept": {
82
+ "command": "uvx",
83
+ "args": ["intercept-mcp"],
84
+ "env": {
85
+ "INTERCEPT_MCP_API_KEY": "hsk_xxxxxxxx"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ## Tools
93
+
94
+ | Name | Description |
95
+ |---|---|
96
+ | `list_repositories` | List repositories in the current tenant. |
97
+ | `get_repository` | Get a repository by ID. |
98
+ | `get_repository_posture` | Get the posture evaluation for a repository. |
99
+ | `list_findings` | List findings by type (sast, secrets, container, iac, pipeline, sbom_vuln), filtered by repository, severity, or open status. |
100
+ | `get_sast_finding` | Get a SAST finding by ID. |
101
+ | `get_finding` | Deprecated alias for `get_sast_finding`. Prefer `get_sast_finding`. |
102
+ | `get_secrets_finding` | Get a secret finding by ID. |
103
+ | `get_container_file` | Get a Dockerfile by ID with its nested security findings. |
104
+ | `get_iac_file` | Get an IaC file by ID with its nested security findings. |
105
+ | `get_pipeline` | Get a CI/CD pipeline by ID with its actions and findings. |
106
+ | `get_sbom_vuln_finding` | Get an SBOM vulnerability (dependency) finding by ID. |
107
+ | `list_scans` | List scans, optionally filtered by repository. |
108
+ | `get_scan` | Get a scan by ID. |
109
+ | `list_organizations` | List organizations in the current tenant. |
110
+ | `get_organization` | Get an organization by slug. |
111
+ | `get_tenant_posture_summary` | Get the tenant-wide posture summary (score, grade, category breakdown). |
112
+ | `update_finding_status` | Update the status and optional note on a finding resolution. |
113
+ | `bulk_update_finding_status` | Bulk-update the status and optional note on up to 500 finding resolutions. |
114
+ | `comment_on_finding` | Attach a note to a finding resolution without changing its status. |
115
+ | `trigger_scan` | Trigger a new scan for a repository. |
116
+
117
+ ## License
118
+
119
+ Proprietary — Hijack Security.
@@ -0,0 +1,88 @@
1
+ # intercept-mcp
2
+
3
+ Model Context Protocol (MCP) server that exposes an [Intercept](https://intercept.hijacksecurity.com) tenant's repositories, findings, scans, and resolutions to MCP-compatible AI clients.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uvx intercept-mcp
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ | Variable | Required | Default |
14
+ |---|---|---|
15
+ | `INTERCEPT_MCP_API_KEY` | yes | — |
16
+ | `INTERCEPT_API_URL` | no | `https://intercept.hijacksecurity.com` |
17
+
18
+ `INTERCEPT_MCP_API_KEY` is a personal-scope API key, generated from the Intercept web UI: **Settings → Integrations → Generate MCP API Key**. The value starts with `hsk_`.
19
+
20
+ `INTERCEPT_API_URL` defaults to Intercept production. Override with the URL provided by your Intercept administrator for other environments.
21
+
22
+ ## Claude Code configuration
23
+
24
+ Export the key from your shell profile (`~/.zshrc`, `~/.bashrc`):
25
+
26
+ ```bash
27
+ export INTERCEPT_MCP_API_KEY=hsk_xxxxxxxx
28
+ ```
29
+
30
+ Then add the server to your MCP client config:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "intercept": {
36
+ "command": "uvx",
37
+ "args": ["intercept-mcp"]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ Restart the client. Verify with `claude mcp list`.
44
+
45
+ If you prefer to keep the key in the client config instead of the shell:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "intercept": {
51
+ "command": "uvx",
52
+ "args": ["intercept-mcp"],
53
+ "env": {
54
+ "INTERCEPT_MCP_API_KEY": "hsk_xxxxxxxx"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Tools
62
+
63
+ | Name | Description |
64
+ |---|---|
65
+ | `list_repositories` | List repositories in the current tenant. |
66
+ | `get_repository` | Get a repository by ID. |
67
+ | `get_repository_posture` | Get the posture evaluation for a repository. |
68
+ | `list_findings` | List findings by type (sast, secrets, container, iac, pipeline, sbom_vuln), filtered by repository, severity, or open status. |
69
+ | `get_sast_finding` | Get a SAST finding by ID. |
70
+ | `get_finding` | Deprecated alias for `get_sast_finding`. Prefer `get_sast_finding`. |
71
+ | `get_secrets_finding` | Get a secret finding by ID. |
72
+ | `get_container_file` | Get a Dockerfile by ID with its nested security findings. |
73
+ | `get_iac_file` | Get an IaC file by ID with its nested security findings. |
74
+ | `get_pipeline` | Get a CI/CD pipeline by ID with its actions and findings. |
75
+ | `get_sbom_vuln_finding` | Get an SBOM vulnerability (dependency) finding by ID. |
76
+ | `list_scans` | List scans, optionally filtered by repository. |
77
+ | `get_scan` | Get a scan by ID. |
78
+ | `list_organizations` | List organizations in the current tenant. |
79
+ | `get_organization` | Get an organization by slug. |
80
+ | `get_tenant_posture_summary` | Get the tenant-wide posture summary (score, grade, category breakdown). |
81
+ | `update_finding_status` | Update the status and optional note on a finding resolution. |
82
+ | `bulk_update_finding_status` | Bulk-update the status and optional note on up to 500 finding resolutions. |
83
+ | `comment_on_finding` | Attach a note to a finding resolution without changing its status. |
84
+ | `trigger_scan` | Trigger a new scan for a repository. |
85
+
86
+ ## License
87
+
88
+ Proprietary — Hijack Security.
@@ -0,0 +1 @@
1
+ 0.2.2
@@ -0,0 +1,16 @@
1
+ """intercept-mcp: MCP server for the Intercept platform."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def _read_version() -> str:
7
+ here = Path(__file__).parent
8
+ for candidate in (here / "VERSION", here.parent / "VERSION"):
9
+ if candidate.exists():
10
+ return candidate.read_text(encoding="utf-8").strip()
11
+ return "0.0.0+unknown"
12
+
13
+
14
+ __version__ = _read_version()
15
+
16
+ __all__ = ["__version__"]
@@ -0,0 +1,175 @@
1
+ """HTTP client for the Intercept REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from intercept_mcp import __version__
11
+
12
+ DEFAULT_BASE_URL = "https://intercept.hijacksecurity.com"
13
+ _CONNECT_TIMEOUT = 30.0
14
+ _READ_TIMEOUT = 60.0
15
+ _MAX_429_RETRIES = 3
16
+
17
+
18
+ class InterceptApiError(Exception):
19
+ """Raised when the Intercept API returns a non-2xx response."""
20
+
21
+ def __init__(self, status_code: int, message: str) -> None:
22
+ super().__init__(message)
23
+ self.status_code = status_code
24
+ self.message = message
25
+
26
+
27
+ def _redact_headers(headers: dict[str, str] | httpx.Headers) -> dict[str, str]:
28
+ """Return a copy of `headers` with credential-bearing values replaced by `[REDACTED]`."""
29
+ redacted: dict[str, str] = {}
30
+ for key, value in dict(headers).items():
31
+ lk = key.lower()
32
+ if lk in ("authorization", "x-api-key", "cookie", "set-cookie"):
33
+ redacted[key] = "[REDACTED]"
34
+ else:
35
+ redacted[key] = value
36
+ return redacted
37
+
38
+
39
+ class InterceptApiClient:
40
+ """Async HTTP client for the Intercept API."""
41
+
42
+ def __init__(
43
+ self,
44
+ api_key: str,
45
+ base_url: str = DEFAULT_BASE_URL,
46
+ *,
47
+ transport: httpx.AsyncBaseTransport | None = None,
48
+ ) -> None:
49
+ if not api_key:
50
+ raise ValueError("api_key must be a non-empty string")
51
+ self._api_key = api_key
52
+ self._base_url = base_url.rstrip("/")
53
+ self._client = httpx.AsyncClient(
54
+ base_url=self._base_url,
55
+ timeout=httpx.Timeout(connect=_CONNECT_TIMEOUT, read=_READ_TIMEOUT, write=_READ_TIMEOUT, pool=_READ_TIMEOUT),
56
+ headers={
57
+ "X-API-Key": api_key,
58
+ "User-Agent": f"intercept-mcp/{__version__}",
59
+ "Accept": "application/json",
60
+ },
61
+ transport=transport,
62
+ )
63
+
64
+ @property
65
+ def base_url(self) -> str:
66
+ return self._base_url
67
+
68
+ async def aclose(self) -> None:
69
+ await self._client.aclose()
70
+
71
+ async def __aenter__(self) -> "InterceptApiClient":
72
+ return self
73
+
74
+ async def __aexit__(self, exc_type, exc, tb) -> None:
75
+ await self.aclose()
76
+
77
+ async def request(
78
+ self,
79
+ method: str,
80
+ path: str,
81
+ *,
82
+ params: dict[str, Any] | None = None,
83
+ json: dict[str, Any] | None = None,
84
+ ) -> Any:
85
+ """Issue an HTTP request and return the parsed JSON body.
86
+
87
+ Raises `InterceptApiError` on non-2xx responses. Retries up to
88
+ `_MAX_429_RETRIES` times on 429, honoring `Retry-After`.
89
+ """
90
+ clean_params = (
91
+ {k: v for k, v in params.items() if v is not None} if params else None
92
+ )
93
+
94
+ attempts = 0
95
+ while True:
96
+ response = await self._client.request(
97
+ method,
98
+ path,
99
+ params=clean_params,
100
+ json=json,
101
+ )
102
+
103
+ if response.status_code == 429 and attempts < _MAX_429_RETRIES:
104
+ retry_after = _parse_retry_after(response.headers.get("Retry-After"))
105
+ attempts += 1
106
+ await asyncio.sleep(retry_after)
107
+ continue
108
+
109
+ if response.is_success:
110
+ if response.status_code == 204 or not response.content:
111
+ return None
112
+ try:
113
+ return response.json()
114
+ except ValueError as exc:
115
+ raise InterceptApiError(
116
+ response.status_code,
117
+ f"Intercept API returned non-JSON body: {exc}",
118
+ ) from exc
119
+
120
+ raise InterceptApiError(
121
+ response.status_code,
122
+ _format_error(response),
123
+ )
124
+
125
+
126
+ def _parse_retry_after(value: str | None) -> float:
127
+ """Parse a numeric `Retry-After` header into seconds. Falls back to 1.0s."""
128
+ if value is None:
129
+ return 1.0
130
+ try:
131
+ return max(0.0, float(value))
132
+ except (TypeError, ValueError):
133
+ return 1.0
134
+
135
+
136
+ def _format_error(response: httpx.Response) -> str:
137
+ """Translate an HTTP error response into a short, readable message."""
138
+ code = response.status_code
139
+ detail = _extract_detail(response)
140
+ if code == 401:
141
+ return (
142
+ "authentication failed: INTERCEPT_MCP_API_KEY is missing or invalid. "
143
+ "Generate a personal-scope key from Settings -> Integrations."
144
+ )
145
+ if code == 403:
146
+ return f"permission denied{_suffix(detail)}"
147
+ if code == 404:
148
+ return f"not found{_suffix(detail)}"
149
+ if code == 422:
150
+ return f"validation error{_suffix(detail)}"
151
+ if code == 429:
152
+ retry = response.headers.get("Retry-After", "?")
153
+ return f"rate limited, retry after {retry}s"
154
+ if 500 <= code < 600:
155
+ return f"server error {code}{_suffix(detail)}"
156
+ return f"HTTP {code}{_suffix(detail)}"
157
+
158
+
159
+ def _extract_detail(response: httpx.Response) -> str | None:
160
+ """Pull `detail` from a JSON error body, or return None."""
161
+ try:
162
+ body = response.json()
163
+ except ValueError:
164
+ return None
165
+ if isinstance(body, dict):
166
+ detail = body.get("detail")
167
+ if isinstance(detail, str):
168
+ return detail
169
+ if detail is not None:
170
+ return str(detail)
171
+ return None
172
+
173
+
174
+ def _suffix(detail: str | None) -> str:
175
+ return f": {detail}" if detail else ""